diff --git a/Cartfile b/Cartfile index 7abfe60..46a4518 100644 --- a/Cartfile +++ b/Cartfile @@ -3,5 +3,5 @@ github "radex/SwiftyUserDefaults" github "libgit2/objective-git" github "zahlz/SwiftPasscodeLock" "master" github "bitserf/FavIcon" -github "kishikawakatsumi/KeychainAccess" "master" +github "kishikawakatsumi/KeychainAccess" github "mattrubin/OneTimePassword" diff --git a/pass.xcodeproj/project.pbxproj b/pass.xcodeproj/project.pbxproj index 12b0373..e0c0996 100644 --- a/pass.xcodeproj/project.pbxproj +++ b/pass.xcodeproj/project.pbxproj @@ -8,10 +8,13 @@ /* Begin PBXBuildFile section */ 94BA784B85E071D25EE89B59 /* libPods-pass.a in Frameworks */ = {isa = PBXBuildFile; fileRef = ADCE7A5C3CCC67D7D21BB3C4 /* libPods-pass.a */; }; + A217ACE21E9AB17C00A1A6CF /* OTPScannerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A217ACE11E9AB17C00A1A6CF /* OTPScannerController.swift */; }; A262A58D1E68749C006B0890 /* Base32.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A262A58C1E68749C006B0890 /* Base32.framework */; }; A27424D91E7C35960093F436 /* NotificationNames.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27424D81E7C35960093F436 /* NotificationNames.swift */; }; A2802BF91E70813A00879216 /* SliderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2802BF71E70813A00879216 /* SliderTableViewCell.swift */; }; A2802BFA1E70813A00879216 /* SliderTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = A2802BF81E70813A00879216 /* SliderTableViewCell.xib */; }; + A2A7813F1E97DBD9001311F5 /* QRScannerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A7813E1E97DBD9001311F5 /* QRScannerController.swift */; }; + A2A89D691E954698003FB2D3 /* UITextFieldExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A89D681E954698003FB2D3 /* UITextFieldExtension.swift */; }; DC037CA61E4B883900609409 /* OpenSourceComponentsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC037CA51E4B883900609409 /* OpenSourceComponentsTableViewController.swift */; }; DC037CA81E4B898100609409 /* BasicStaticTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC037CA71E4B898100609409 /* BasicStaticTableViewController.swift */; }; DC037CAA1E4B8EAE00609409 /* SpecialThanksTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC037CA91E4B8EAE00609409 /* SpecialThanksTableViewController.swift */; }; @@ -80,10 +83,13 @@ /* Begin PBXFileReference section */ 274CCFCF32444A2FF46BE7F4 /* Pods-pass.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-pass.debug.xcconfig"; path = "Pods/Target Support Files/Pods-pass/Pods-pass.debug.xcconfig"; sourceTree = ""; }; + A217ACE11E9AB17C00A1A6CF /* OTPScannerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTPScannerController.swift; sourceTree = ""; }; A262A58C1E68749C006B0890 /* Base32.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Base32.framework; path = Carthage/Build/iOS/Base32.framework; sourceTree = ""; }; A27424D81E7C35960093F436 /* NotificationNames.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationNames.swift; sourceTree = ""; }; A2802BF71E70813A00879216 /* SliderTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderTableViewCell.swift; sourceTree = ""; }; A2802BF81E70813A00879216 /* SliderTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SliderTableViewCell.xib; sourceTree = ""; }; + A2A7813E1E97DBD9001311F5 /* QRScannerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRScannerController.swift; sourceTree = ""; }; + A2A89D681E954698003FB2D3 /* UITextFieldExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITextFieldExtension.swift; sourceTree = ""; }; ADCE7A5C3CCC67D7D21BB3C4 /* libPods-pass.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-pass.a"; sourceTree = BUILT_PRODUCTS_DIR; }; AEAD6B31EAF5D061447A68CC /* Pods-pass.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-pass.release.xcconfig"; path = "Pods/Target Support Files/Pods-pass/Pods-pass.release.xcconfig"; sourceTree = ""; }; DC037CA51E4B883900609409 /* OpenSourceComponentsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenSourceComponentsTableViewController.swift; sourceTree = ""; }; @@ -215,6 +221,8 @@ DC037CA91E4B8EAE00609409 /* SpecialThanksTableViewController.swift */, DC8963BF1E38EEB900828B09 /* SSHKeySettingTableViewController.swift */, DCC441531E916382008A90C4 /* GitSSHKeyArmorSettingTableViewController.swift */, + A2A7813E1E97DBD9001311F5 /* QRScannerController.swift */, + A217ACE11E9AB17C00A1A6CF /* OTPScannerController.swift */, ); path = Controllers; sourceTree = ""; @@ -239,6 +247,7 @@ DCA049971E33586A00522E8F /* DefaultsKeys.swift */, DC19400A1E4B36B60077E0A3 /* Utils.swift */, A27424D81E7C35960093F436 /* NotificationNames.swift */, + A2A89D681E954698003FB2D3 /* UITextFieldExtension.swift */, ); path = Helpers; sourceTree = ""; @@ -524,9 +533,11 @@ DC5F385B1E56AADB00C69ACA /* PGPKeyArmorSettingTableViewController.swift in Sources */, A27424D91E7C35960093F436 /* NotificationNames.swift in Sources */, DCAAF7451E2FA66800AB94BC /* SettingsTableViewController.swift in Sources */, + A217ACE21E9AB17C00A1A6CF /* OTPScannerController.swift in Sources */, DCFB77A71E502DF9008DE471 /* EditPasswordTableViewController.swift in Sources */, DCA0499A1E335CC800522E8F /* GitServerSettingTableViewController.swift in Sources */, DCDDEAB31E4896BF00F68193 /* PasswordDetailTitleTableViewCell.swift in Sources */, + A2A7813F1E97DBD9001311F5 /* QRScannerController.swift in Sources */, DCC277D21E30D6EA00402246 /* pass.xcdatamodeld in Sources */, DC4914991E434600007FF592 /* PasswordDetailTableViewController.swift in Sources */, DC962CDF1E4B62C10033B5D8 /* AboutTableViewController.swift in Sources */, @@ -544,6 +555,7 @@ DC1940001E49E1A60077E0A3 /* PasscodeLockConfiguration.swift in Sources */, DC917BD71E2E8231000FDF54 /* AppDelegate.swift in Sources */, DCFB779A1E4F3BCF008DE471 /* TitleTextFieldTableViewCell.swift in Sources */, + A2A89D691E954698003FB2D3 /* UITextFieldExtension.swift in Sources */, DC037CBB1E4DD47B00609409 /* TextFieldTableViewCell.swift in Sources */, DC193FFE1E49E0760077E0A3 /* PasscodeLockRepository.swift in Sources */, DCA049981E33586A00522E8F /* DefaultsKeys.swift in Sources */, diff --git a/pass/Base.lproj/Main.storyboard b/pass/Base.lproj/Main.storyboard index f91974e..c516009 100644 --- a/pass/Base.lproj/Main.storyboard +++ b/pass/Base.lproj/Main.storyboard @@ -1,11 +1,11 @@ - + - + @@ -61,7 +61,7 @@ - + @@ -72,7 +72,7 @@ - + @@ -244,7 +244,7 @@ - + @@ -275,45 +275,52 @@ - + - + - - + + - - + + - - - - - - - + + + + + + + + + + @@ -323,10 +330,11 @@ + - + @@ -339,25 +347,29 @@ - - + + - - + + - - + + - + + + + + - + @@ -365,25 +377,28 @@ - - + + - - + + - - + + - + + + + - + @@ -392,10 +407,10 @@ - + - + - + - + @@ -504,7 +519,7 @@ - + @@ -534,7 +553,7 @@ - + @@ -589,7 +611,7 @@ - + @@ -617,11 +639,65 @@ + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -644,7 +720,7 @@ - + @@ -663,7 +739,7 @@ - + @@ -703,7 +779,7 @@ - + @@ -717,7 +793,7 @@ - + @@ -731,17 +807,18 @@ + - + - + - + @@ -750,10 +827,14 @@ - - + + - Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + K7PBbkoaJf6mLyVX3EBU +username: passforios-demo@email.com +URL: https://www.amazon.com +Secret Question 1: What is your childhood best friend's most bizarre superhero fantasy? Oh god, Amazon, it's too awful to say... +Phone Support PIN #: 84719 @@ -762,11 +843,11 @@ - + - + @@ -780,9 +861,9 @@ - + - + @@ -796,13 +877,16 @@ + + + - + @@ -820,9 +904,9 @@ - + - + @@ -837,7 +921,7 @@ - + @@ -867,7 +955,7 @@ - + @@ -901,6 +992,13 @@ + + + + + + + @@ -908,9 +1006,9 @@ - + - + @@ -924,13 +1022,14 @@ + - + @@ -948,9 +1047,9 @@ - + - + @@ -1028,6 +1127,7 @@ + @@ -1036,9 +1136,9 @@ - + - + @@ -1052,6 +1152,7 @@ + @@ -1059,9 +1160,9 @@ - + - + @@ -1075,12 +1176,13 @@ + - + - + @@ -1094,10 +1196,11 @@ + - + @@ -1115,7 +1218,7 @@ - + @@ -1133,71 +1236,77 @@ - + - + - + - + - - - + + - - - - - - - - - - @@ -1205,47 +1314,21 @@ - + - + - -----BEGIN PGP PUBLIC KEY BLOCK----- - -mQENBFiiqFoBCACzvPOW+J1RlQOI9oB7Iwuk92rzpkKGC8tiwo4otHMc3rxmzYlG -gEdHmSi6Ix0+hYZ2vSWJamPQl9WUiYSBQmkVwoqiwXNWcqRePOylcqsj58owIxZt -23nXUIG78t+5BatoWyl9/qXsMUMSink7RAlOnjCyvoSJw9yzUVeWn723rvAr7ypV -yqjce4gOGIfGT8PpGXThDQjvg4gM+wxUD3+oNhW50Qb+crMsefxwEoGXbCA4+6ZO -nJGy2MhRr4FkXwNAAcvTvTmYXQfH8Q8xCtgl4PyxQA4OAeoEfxl5FOuOGIbNKgMs -133n2oue5dy3YERN2t9/Di8Iov4Bsc+nj6+vABEBAAG0E1Bhc3MgZm9yIGlPUyAo -RGVtbymJATcEEwEKACEFAliiqFoCGwMFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AA -CgkQl/oT92AAdt0TqAgAlTsGLTe9+qY9pfyBVpDk+z1SC9kaOYsvoj7hSWVOXxwP -yn4jxjHUr1C9fyaZqP9K4yIdl9jsGBkpaR6lhnUm9x5RO5gEAAAaLhx3ZgE7/XQg -y7WHibxsnLaTEMVnNh8BJzQveYSqO8frBkQofj0K9LNiqWHhcllKKNkVHqyeMRII -KYPOXvDnsnKqywapu5VyyErMnPqq6h7i3Z7+xDfVk8iUew9i0LR/QEmJO7iTH98C -DczQo+4VnflBIHiNcMaXQqC2Li5rS+O3a9hq5V/gJwBtIx1iXvDaRATH4aUANxTZ -POHVLwxBuTwilhnEaAPGkGu+oG1XF+RWOyrGYluWc7kBDQRYoqhaAQgAoh9B+sY1 -jZZgnqP11a9wkjom2DcBJLreYNzgBFYhUcmxtq+ZWyorPFzMJpbIjzMGRiGWFIus -DE5D5LZdAvkINWKsbVN6Y3OI6Tj+/O45SuJ7EusG8Z1bwmjJqHy/Y//Fk0rF6ZFs -g0+p12Xzr5k8sMKUU4I5T4DNrsS+mwnOPItC7NGawkArPD7Mew0ma+Luv7b3g0gA -DuofvxKiN0jq6W9QPUNRY4BKm+Y03CdZNXyMDs/NTguDdfarawTIAIlJOSg5rV7S -74v73dtZkB5LySX1rBomRb5zkCg6/Ygap4RO1aO9gbXyPmc2lUQSBAJCS9d8h0qD -sNGembqhGDwHpQARAQABiQEfBBgBCgAJBQJYoqhaAhsMAAoJEJf6E/dgAHbdKl4H -/A/Q+hOh40M2B6Sh/Rsus+k1VL51+UXVJRyiNiqvofamufF5ULfeq3Pt5HfqpCsF -/A7iRP0MuanKcZbLDjDv6wWDuRvW9Kwn5qjbb6iBPh+wfyReqNMWGksJTyap0SNR -+Mn+sjbxGkUPjzVdRdqtw64R3DLW6SpMNIUVC6paazCViEtHsSauWwP/0+DRA/6j -xBHSAr0UaLsRTHleOSWV6ABrh9skJRWLBcOoi5pxotBs/3BmjqERQf9bwmmyxj2K -cE60Muv6qY4/KCQqPKhyFjJwXhS/e7BwxHEt1SbUb1aC4lH5UrbW9KYLu5lHJRdY -pfZ36xQbOAQYKKf6ZTT5R/Y= -=2pJ8 ------END PGP PUBLIC KEY BLOCK----- + + + @@ -1261,47 +1344,21 @@ pfZ36xQbOAQYKKf6ZTT5R/Y= - + - + - + - -----BEGIN PGP PUBLIC KEY BLOCK----- - -mQENBFiiqFoBCACzvPOW+J1RlQOI9oB7Iwuk92rzpkKGC8tiwo4otHMc3rxmzYlG -gEdHmSi6Ix0+hYZ2vSWJamPQl9WUiYSBQmkVwoqiwXNWcqRePOylcqsj58owIxZt -23nXUIG78t+5BatoWyl9/qXsMUMSink7RAlOnjCyvoSJw9yzUVeWn723rvAr7ypV -yqjce4gOGIfGT8PpGXThDQjvg4gM+wxUD3+oNhW50Qb+crMsefxwEoGXbCA4+6ZO -nJGy2MhRr4FkXwNAAcvTvTmYXQfH8Q8xCtgl4PyxQA4OAeoEfxl5FOuOGIbNKgMs -133n2oue5dy3YERN2t9/Di8Iov4Bsc+nj6+vABEBAAG0E1Bhc3MgZm9yIGlPUyAo -RGVtbymJATcEEwEKACEFAliiqFoCGwMFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AA -CgkQl/oT92AAdt0TqAgAlTsGLTe9+qY9pfyBVpDk+z1SC9kaOYsvoj7hSWVOXxwP -yn4jxjHUr1C9fyaZqP9K4yIdl9jsGBkpaR6lhnUm9x5RO5gEAAAaLhx3ZgE7/XQg -y7WHibxsnLaTEMVnNh8BJzQveYSqO8frBkQofj0K9LNiqWHhcllKKNkVHqyeMRII -KYPOXvDnsnKqywapu5VyyErMnPqq6h7i3Z7+xDfVk8iUew9i0LR/QEmJO7iTH98C -DczQo+4VnflBIHiNcMaXQqC2Li5rS+O3a9hq5V/gJwBtIx1iXvDaRATH4aUANxTZ -POHVLwxBuTwilhnEaAPGkGu+oG1XF+RWOyrGYluWc7kBDQRYoqhaAQgAoh9B+sY1 -jZZgnqP11a9wkjom2DcBJLreYNzgBFYhUcmxtq+ZWyorPFzMJpbIjzMGRiGWFIus -DE5D5LZdAvkINWKsbVN6Y3OI6Tj+/O45SuJ7EusG8Z1bwmjJqHy/Y//Fk0rF6ZFs -g0+p12Xzr5k8sMKUU4I5T4DNrsS+mwnOPItC7NGawkArPD7Mew0ma+Luv7b3g0gA -DuofvxKiN0jq6W9QPUNRY4BKm+Y03CdZNXyMDs/NTguDdfarawTIAIlJOSg5rV7S -74v73dtZkB5LySX1rBomRb5zkCg6/Ygap4RO1aO9gbXyPmc2lUQSBAJCS9d8h0qD -sNGembqhGDwHpQARAQABiQEfBBgBCgAJBQJYoqhaAhsMAAoJEJf6E/dgAHbdKl4H -/A/Q+hOh40M2B6Sh/Rsus+k1VL51+UXVJRyiNiqvofamufF5ULfeq3Pt5HfqpCsF -/A7iRP0MuanKcZbLDjDv6wWDuRvW9Kwn5qjbb6iBPh+wfyReqNMWGksJTyap0SNR -+Mn+sjbxGkUPjzVdRdqtw64R3DLW6SpMNIUVC6paazCViEtHsSauWwP/0+DRA/6j -xBHSAr0UaLsRTHleOSWV6ABrh9skJRWLBcOoi5pxotBs/3BmjqERQf9bwmmyxj2K -cE60Muv6qY4/KCQqPKhyFjJwXhS/e7BwxHEt1SbUb1aC4lH5UrbW9KYLu5lHJRdY -pfZ36xQbOAQYKKf6ZTT5R/Y= -=2pJ8 ------END PGP PUBLIC KEY BLOCK----- + + + @@ -1341,7 +1398,7 @@ pfZ36xQbOAQYKKf6ZTT5R/Y= - + @@ -1359,7 +1416,7 @@ pfZ36xQbOAQYKKf6ZTT5R/Y= - + @@ -1377,71 +1434,60 @@ pfZ36xQbOAQYKKf6ZTT5R/Y= - + - + - + - + - + - - - + + - - - - - - - - - - @@ -1449,44 +1495,15 @@ pfZ36xQbOAQYKKf6ZTT5R/Y= - + - + - -----BEGIN PGP PUBLIC KEY BLOCK----- - -mQENBFiiqFoBCACzvPOW+J1RlQOI9oB7Iwuk92rzpkKGC8tiwo4otHMc3rxmzYlG -gEdHmSi6Ix0+hYZ2vSWJamPQl9WUiYSBQmkVwoqiwXNWcqRePOylcqsj58owIxZt -23nXUIG78t+5BatoWyl9/qXsMUMSink7RAlOnjCyvoSJw9yzUVeWn723rvAr7ypV -yqjce4gOGIfGT8PpGXThDQjvg4gM+wxUD3+oNhW50Qb+crMsefxwEoGXbCA4+6ZO -nJGy2MhRr4FkXwNAAcvTvTmYXQfH8Q8xCtgl4PyxQA4OAeoEfxl5FOuOGIbNKgMs -133n2oue5dy3YERN2t9/Di8Iov4Bsc+nj6+vABEBAAG0E1Bhc3MgZm9yIGlPUyAo -RGVtbymJATcEEwEKACEFAliiqFoCGwMFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AA -CgkQl/oT92AAdt0TqAgAlTsGLTe9+qY9pfyBVpDk+z1SC9kaOYsvoj7hSWVOXxwP -yn4jxjHUr1C9fyaZqP9K4yIdl9jsGBkpaR6lhnUm9x5RO5gEAAAaLhx3ZgE7/XQg -y7WHibxsnLaTEMVnNh8BJzQveYSqO8frBkQofj0K9LNiqWHhcllKKNkVHqyeMRII -KYPOXvDnsnKqywapu5VyyErMnPqq6h7i3Z7+xDfVk8iUew9i0LR/QEmJO7iTH98C -DczQo+4VnflBIHiNcMaXQqC2Li5rS+O3a9hq5V/gJwBtIx1iXvDaRATH4aUANxTZ -POHVLwxBuTwilhnEaAPGkGu+oG1XF+RWOyrGYluWc7kBDQRYoqhaAQgAoh9B+sY1 -jZZgnqP11a9wkjom2DcBJLreYNzgBFYhUcmxtq+ZWyorPFzMJpbIjzMGRiGWFIus -DE5D5LZdAvkINWKsbVN6Y3OI6Tj+/O45SuJ7EusG8Z1bwmjJqHy/Y//Fk0rF6ZFs -g0+p12Xzr5k8sMKUU4I5T4DNrsS+mwnOPItC7NGawkArPD7Mew0ma+Luv7b3g0gA -DuofvxKiN0jq6W9QPUNRY4BKm+Y03CdZNXyMDs/NTguDdfarawTIAIlJOSg5rV7S -74v73dtZkB5LySX1rBomRb5zkCg6/Ygap4RO1aO9gbXyPmc2lUQSBAJCS9d8h0qD -sNGembqhGDwHpQARAQABiQEfBBgBCgAJBQJYoqhaAhsMAAoJEJf6E/dgAHbdKl4H -/A/Q+hOh40M2B6Sh/Rsus+k1VL51+UXVJRyiNiqvofamufF5ULfeq3Pt5HfqpCsF -/A7iRP0MuanKcZbLDjDv6wWDuRvW9Kwn5qjbb6iBPh+wfyReqNMWGksJTyap0SNR -+Mn+sjbxGkUPjzVdRdqtw64R3DLW6SpMNIUVC6paazCViEtHsSauWwP/0+DRA/6j -xBHSAr0UaLsRTHleOSWV6ABrh9skJRWLBcOoi5pxotBs/3BmjqERQf9bwmmyxj2K -cE60Muv6qY4/KCQqPKhyFjJwXhS/e7BwxHEt1SbUb1aC4lH5UrbW9KYLu5lHJRdY -pfZ36xQbOAQYKKf6ZTT5R/Y= -=2pJ8 ------END PGP PUBLIC KEY BLOCK----- @@ -1505,44 +1522,15 @@ pfZ36xQbOAQYKKf6ZTT5R/Y= - + - + - -----BEGIN PGP PUBLIC KEY BLOCK----- - -mQENBFiiqFoBCACzvPOW+J1RlQOI9oB7Iwuk92rzpkKGC8tiwo4otHMc3rxmzYlG -gEdHmSi6Ix0+hYZ2vSWJamPQl9WUiYSBQmkVwoqiwXNWcqRePOylcqsj58owIxZt -23nXUIG78t+5BatoWyl9/qXsMUMSink7RAlOnjCyvoSJw9yzUVeWn723rvAr7ypV -yqjce4gOGIfGT8PpGXThDQjvg4gM+wxUD3+oNhW50Qb+crMsefxwEoGXbCA4+6ZO -nJGy2MhRr4FkXwNAAcvTvTmYXQfH8Q8xCtgl4PyxQA4OAeoEfxl5FOuOGIbNKgMs -133n2oue5dy3YERN2t9/Di8Iov4Bsc+nj6+vABEBAAG0E1Bhc3MgZm9yIGlPUyAo -RGVtbymJATcEEwEKACEFAliiqFoCGwMFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AA -CgkQl/oT92AAdt0TqAgAlTsGLTe9+qY9pfyBVpDk+z1SC9kaOYsvoj7hSWVOXxwP -yn4jxjHUr1C9fyaZqP9K4yIdl9jsGBkpaR6lhnUm9x5RO5gEAAAaLhx3ZgE7/XQg -y7WHibxsnLaTEMVnNh8BJzQveYSqO8frBkQofj0K9LNiqWHhcllKKNkVHqyeMRII -KYPOXvDnsnKqywapu5VyyErMnPqq6h7i3Z7+xDfVk8iUew9i0LR/QEmJO7iTH98C -DczQo+4VnflBIHiNcMaXQqC2Li5rS+O3a9hq5V/gJwBtIx1iXvDaRATH4aUANxTZ -POHVLwxBuTwilhnEaAPGkGu+oG1XF+RWOyrGYluWc7kBDQRYoqhaAQgAoh9B+sY1 -jZZgnqP11a9wkjom2DcBJLreYNzgBFYhUcmxtq+ZWyorPFzMJpbIjzMGRiGWFIus -DE5D5LZdAvkINWKsbVN6Y3OI6Tj+/O45SuJ7EusG8Z1bwmjJqHy/Y//Fk0rF6ZFs -g0+p12Xzr5k8sMKUU4I5T4DNrsS+mwnOPItC7NGawkArPD7Mew0ma+Luv7b3g0gA -DuofvxKiN0jq6W9QPUNRY4BKm+Y03CdZNXyMDs/NTguDdfarawTIAIlJOSg5rV7S -74v73dtZkB5LySX1rBomRb5zkCg6/Ygap4RO1aO9gbXyPmc2lUQSBAJCS9d8h0qD -sNGembqhGDwHpQARAQABiQEfBBgBCgAJBQJYoqhaAhsMAAoJEJf6E/dgAHbdKl4H -/A/Q+hOh40M2B6Sh/Rsus+k1VL51+UXVJRyiNiqvofamufF5ULfeq3Pt5HfqpCsF -/A7iRP0MuanKcZbLDjDv6wWDuRvW9Kwn5qjbb6iBPh+wfyReqNMWGksJTyap0SNR -+Mn+sjbxGkUPjzVdRdqtw64R3DLW6SpMNIUVC6paazCViEtHsSauWwP/0+DRA/6j -xBHSAr0UaLsRTHleOSWV6ABrh9skJRWLBcOoi5pxotBs/3BmjqERQf9bwmmyxj2K -cE60Muv6qY4/KCQqPKhyFjJwXhS/e7BwxHEt1SbUb1aC4lH5UrbW9KYLu5lHJRdY -pfZ36xQbOAQYKKf6ZTT5R/Y= -=2pJ8 ------END PGP PUBLIC KEY BLOCK----- @@ -1564,6 +1552,13 @@ pfZ36xQbOAQYKKf6ZTT5R/Y= + + + + + + + @@ -1571,11 +1566,32 @@ pfZ36xQbOAQYKKf6ZTT5R/Y= - + + + + + + + + + + + + + + + + + + + + + + diff --git a/pass/Controllers/AboutRepositoryTableViewController.swift b/pass/Controllers/AboutRepositoryTableViewController.swift index 7789a6f..55deb52 100644 --- a/pass/Controllers/AboutRepositoryTableViewController.swift +++ b/pass/Controllers/AboutRepositoryTableViewController.swift @@ -18,7 +18,6 @@ class AboutRepositoryTableViewController: BasicStaticTableViewController { private let passwordStore = PasswordStore.shared override func viewDidLoad() { - navigationItemTitle = "About Repository" super.viewDidLoad() indicator.center = CGPoint(x: view.bounds.midX, y: view.bounds.height * 0.382) diff --git a/pass/Controllers/AboutTableViewController.swift b/pass/Controllers/AboutTableViewController.swift index 2562149..7ff79e2 100644 --- a/pass/Controllers/AboutTableViewController.swift +++ b/pass/Controllers/AboutTableViewController.swift @@ -21,7 +21,6 @@ class AboutTableViewController: BasicStaticTableViewController { [[.title: "Open Source Components", .action: "segue", .link: "showOpenSourceComponentsSegue"], [.title: "Special Thanks", .action: "segue", .link: "showSpecialThanksSegue"],], ] - navigationItemTitle = "About" super.viewDidLoad() } diff --git a/pass/Controllers/AddPasswordTableViewController.swift b/pass/Controllers/AddPasswordTableViewController.swift index bb47726..dea0558 100644 --- a/pass/Controllers/AddPasswordTableViewController.swift +++ b/pass/Controllers/AddPasswordTableViewController.swift @@ -19,6 +19,7 @@ class AddPasswordTableViewController: PasswordEditorTableViewController { [[.type: PasswordEditorCellType.fillPasswordCell, .title: "password"], [.type: PasswordEditorCellType.passwordLengthCell, .title: "passwordlength"]], [[.type: PasswordEditorCellType.textViewCell, .title: "additions"]], + [[.type: PasswordEditorCellType.scanQRCodeCell]] ] super.viewDidLoad() } @@ -54,13 +55,15 @@ class AddPasswordTableViewController: PasswordEditorTableViewController { } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - if segue.identifier == "saveAddPasswordSegue" {let cells = tableView.visibleCells + super.prepare(for: segue, sender: sender) + if segue.identifier == "saveAddPasswordSegue" { + let cells = tableView.visibleCells var cellContents = [String: String]() for cell in cells { - let indexPath = tableView.indexPath(for: cell)! - let contentCell = cell as! ContentTableViewCell - let cellTitle = tableData[indexPath.section][indexPath.row][.title] as! String - if let cellContent = contentCell.getContent() { + if let indexPath = tableView.indexPath(for: cell), + let contentCell = cell as? ContentTableViewCell, + let cellTitle = tableData[indexPath.section][indexPath.row][.title] as? String, + let cellContent = contentCell.getContent() { cellContents[cellTitle] = cellContent } } diff --git a/pass/Controllers/AdvancedSettingsTableViewController.swift b/pass/Controllers/AdvancedSettingsTableViewController.swift index c3ddc76..d8e5afd 100644 --- a/pass/Controllers/AdvancedSettingsTableViewController.swift +++ b/pass/Controllers/AdvancedSettingsTableViewController.swift @@ -27,7 +27,6 @@ class AdvancedSettingsTableViewController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() - navigationItem.title = "Advanced" encryptInASCIIArmoredSwitch.isOn = Defaults[.encryptInArmored] encryptInASCIIArmoredTableViewCell.accessoryView = encryptInASCIIArmoredSwitch encryptInASCIIArmoredTableViewCell.selectionStyle = .none diff --git a/pass/Controllers/BasicStaticTableViewController.swift b/pass/Controllers/BasicStaticTableViewController.swift index e7e184a..a0d7536 100644 --- a/pass/Controllers/BasicStaticTableViewController.swift +++ b/pass/Controllers/BasicStaticTableViewController.swift @@ -29,7 +29,9 @@ class BasicStaticTableViewController: UITableViewController, MFMailComposeViewCo override func viewDidLoad() { super.viewDidLoad() - navigationItem.title = navigationItemTitle + if navigationItemTitle != nil { + navigationItem.title = navigationItemTitle + } } override func numberOfSections(in tableView: UITableView) -> Int { @@ -54,6 +56,7 @@ class BasicStaticTableViewController: UITableViewController, MFMailComposeViewCo switch cellDataStyle ?? .defaultStyle { case .value1: cell = UITableViewCell(style: .value1, reuseIdentifier: "value1 cell") + cell?.selectionStyle = .none default: cell = UITableViewCell(style: .default, reuseIdentifier: "default cell") } @@ -65,6 +68,7 @@ class BasicStaticTableViewController: UITableViewController, MFMailComposeViewCo cell?.accessoryType = accessoryType } else { cell?.accessoryType = .disclosureIndicator + cell?.selectionStyle = .default } cell?.textLabel?.text = cellData[CellDataKey.title] as? String diff --git a/pass/Controllers/CommitLogsTableViewController.swift b/pass/Controllers/CommitLogsTableViewController.swift index 4a5088d..2255c99 100644 --- a/pass/Controllers/CommitLogsTableViewController.swift +++ b/pass/Controllers/CommitLogsTableViewController.swift @@ -16,8 +16,8 @@ class CommitLogsTableViewController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() commits = passwordStore.getRecentCommits(count: 20) - navigationItem.title = "Recent Commit Logs" - navigationController!.navigationBar.topItem!.title = "About" + self.tableView.estimatedRowHeight = 50 + self.tableView.rowHeight = UITableViewAutomaticDimension } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { @@ -27,13 +27,16 @@ class CommitLogsTableViewController: UITableViewController { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "commitLogCell", for: indexPath) let formatter = DateFormatter() - formatter.dateStyle = DateFormatter.Style.short - formatter.timeStyle = .none + formatter.dateStyle = DateFormatter.Style.medium + formatter.timeStyle = .medium let dateString = formatter.string(from: commits[indexPath.row].commitDate) - let dateLabel = cell.viewWithTag(101) as! UILabel - let messageLabel = cell.viewWithTag(102) as! UILabel - dateLabel.text = dateString - messageLabel.text = commits[indexPath.row].message + + let author = cell.contentView.viewWithTag(100) as? UILabel + let dateLabel = cell.contentView.viewWithTag(101) as? UILabel + let messageLabel = cell.contentView.viewWithTag(102) as? UILabel + author?.text = commits[indexPath.row].author?.name + dateLabel?.text = dateString + messageLabel?.text = commits[indexPath.row].message?.trimmingCharacters(in: .whitespacesAndNewlines) return cell } } diff --git a/pass/Controllers/EditPasswordTableViewController.swift b/pass/Controllers/EditPasswordTableViewController.swift index 71b6146..0caf2f8 100644 --- a/pass/Controllers/EditPasswordTableViewController.swift +++ b/pass/Controllers/EditPasswordTableViewController.swift @@ -15,7 +15,8 @@ class EditPasswordTableViewController: PasswordEditorTableViewController { [[.type: PasswordEditorCellType.fillPasswordCell, .title: "password", .content: password!.password], [.type: PasswordEditorCellType.passwordLengthCell, .title: "passwordlength"]], [[.type: PasswordEditorCellType.textViewCell, .title: "additions", .content: password!.getAdditionsPlainText()]], - [[.type: PasswordEditorCellType.deletePasswordCell]], + [[.type: PasswordEditorCellType.scanQRCodeCell], + [.type: PasswordEditorCellType.deletePasswordCell]] ] super.viewDidLoad() } @@ -37,16 +38,16 @@ class EditPasswordTableViewController: PasswordEditorTableViewController { } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + super.prepare(for: segue, sender: sender) if segue.identifier == "saveEditPasswordSegue" { let cells = tableView.visibleCells var cellContents = [String: String]() for cell in cells { - let indexPath = tableView.indexPath(for: cell)! - if let contentCell = cell as? ContentTableViewCell { - let cellTitle = tableData[indexPath.section][indexPath.row][.title] as! String - if let cellContent = contentCell.getContent() { - cellContents[cellTitle] = cellContent - } + if let indexPath = tableView.indexPath(for: cell), + let contentCell = cell as? ContentTableViewCell, + let cellTitle = tableData[indexPath.section][indexPath.row][.title] as? String, + let cellContent = contentCell.getContent() { + cellContents[cellTitle] = cellContent } } var plainText = "" diff --git a/pass/Controllers/GeneralSettingsTableViewController.swift b/pass/Controllers/GeneralSettingsTableViewController.swift index df1bede..686fe1d 100644 --- a/pass/Controllers/GeneralSettingsTableViewController.swift +++ b/pass/Controllers/GeneralSettingsTableViewController.swift @@ -47,7 +47,6 @@ class GeneralSettingsTableViewController: BasicStaticTableViewController { }() override func viewDidLoad() { - navigationItemTitle = "General" tableData = [ // section 0 [[.title: "About Repository", .action: "segue", .link: "showAboutRepositorySegue"],], diff --git a/pass/Controllers/GitSSHKeyArmorSettingTableViewController.swift b/pass/Controllers/GitSSHKeyArmorSettingTableViewController.swift index cb505c0..035f73a 100644 --- a/pass/Controllers/GitSSHKeyArmorSettingTableViewController.swift +++ b/pass/Controllers/GitSSHKeyArmorSettingTableViewController.swift @@ -9,12 +9,13 @@ import UIKit import SwiftyUserDefaults -class GitSSHKeyArmorSettingTableViewController: UITableViewController { +class GitSSHKeyArmorSettingTableViewController: UITableViewController, UITextViewDelegate { @IBOutlet weak var armorPublicKeyTextView: UITextView! @IBOutlet weak var armorPrivateKeyTextView: UITextView! var gitSSHPrivateKeyPassphrase: String? let passwordStore = PasswordStore.shared - var doneBarButtonItem: UIBarButtonItem? + + private var recentPastedText = "" override func viewDidLoad() { super.viewDidLoad() @@ -22,12 +23,8 @@ class GitSSHKeyArmorSettingTableViewController: UITableViewController { armorPrivateKeyTextView.text = Defaults[.gitSSHPrivateKeyArmor] gitSSHPrivateKeyPassphrase = passwordStore.gitSSHPrivateKeyPassphrase - doneBarButtonItem = UIBarButtonItem(title: "Done", - style: UIBarButtonItemStyle.done, - target: self, - action: #selector(doneButtonTapped(_:))) - navigationItem.rightBarButtonItem = doneBarButtonItem - navigationItem.title = "SSH Key" + armorPublicKeyTextView.delegate = self + armorPrivateKeyTextView.delegate = self } private func createSavePassphraseAlert() -> UIAlertController { @@ -46,7 +43,7 @@ class GitSSHKeyArmorSettingTableViewController: UITableViewController { return savePassphraseAlert } - func doneButtonTapped(_ sender: Any) { + @IBAction func doneButtonTapped(_ sender: Any) { Defaults[.gitSSHPublicKeyArmor] = armorPublicKeyTextView.text Defaults[.gitSSHPrivateKeyArmor] = armorPrivateKeyTextView.text do { @@ -68,5 +65,17 @@ class GitSSHKeyArmorSettingTableViewController: UITableViewController { self.present(alert, animated: true, completion: nil) } - + func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + if text == UIPasteboard.general.string { + // user pastes somethint, get ready to clear in 10s + recentPastedText = text + DispatchQueue.global(qos: .background).asyncAfter(deadline: DispatchTime.now() + 10) { [weak weakSelf = self] in + if let pasteboardString = UIPasteboard.general.string, + pasteboardString == weakSelf?.recentPastedText { + UIPasteboard.general.string = "" + } + } + } + return true + } } diff --git a/pass/Controllers/OTPScannerController.swift b/pass/Controllers/OTPScannerController.swift new file mode 100644 index 0000000..f9c71da --- /dev/null +++ b/pass/Controllers/OTPScannerController.swift @@ -0,0 +1,83 @@ +// +// QRScannerController.swift +// pass +// +// Created by Yishi Lin on 10/4/17. +// Copyright © 2017 Yishi Lin. All rights reserved. +// + +import UIKit +import AVFoundation + +class OTPScannerController: QRScannerController { + + var tempPassword: Password? + var scannedOTP: String? + + // MARK: - AVCaptureMetadataOutputObjectsDelegate Methods + override func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [Any]!, from connection: AVCaptureConnection!) { + + if let metadataObj = metadataObjects.first as? AVMetadataMachineReadableCodeObject, + supportedCodeTypes.contains(metadataObj.type), + let barCodeObject = videoPreviewLayer?.transformedMetadataObject(for: metadataObj) { + + // draw a bounds on the found QR code + qrCodeFrameView?.frame = barCodeObject.bounds + + // check whether it is a valid result + if let scannedString = metadataObj.stringValue { + if let (accept, message) = delegate?.checkScannedOutput(line: scannedString) { + if accept == true { + captureSession?.stopRunning() + scannedOTP = scannedString + tempPassword = Password(name: "empty", plainText: scannedString) + // set scannerOutput + setupOneTimePasswordMessage() + } else { + scannerOutput.text = message + } + } else { + // no delegate, show the scanned result + scannerOutput.text = scannedString + } + } else { + scannerOutput.text = "No string value" + } + + } else { + qrCodeFrameView?.frame = CGRect.zero + scannerOutput.text = "No QR code detected" + } + } + + private func setupOneTimePasswordMessage() { + if let password = tempPassword { + if password.otpType == .hotp { + // hotp, no need to refresh + let (title, content) = password.getOtpStrings()! + scannerOutput.text = "\(title):\(content)" + } else if password.otpType == .totp { + // totp, refresh + Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { + [weak weakSelf = self] timer in + let (title, content) = password.getOtpStrings()! + weakSelf?.scannerOutput.text = "\(title):\(content)" + } + } + } + } + + override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { + if identifier == "saveAddScannedOTPSegue" { + return tempPassword != nil + } + return true + } + +// override func prepare(for segue: UIStoryboardSegue, sender: Any?) { +// super.prepare(for: segue, sender: sender) +// if segue.identifier == "saveAddScannedOTPSegue" { +// delegate?.handleScannedOutput(line: scannedOTP) +// } +// } +} diff --git a/pass/Controllers/OpenSourceComponentsTableViewController.swift b/pass/Controllers/OpenSourceComponentsTableViewController.swift index 0dcc64d..6bffac5 100644 --- a/pass/Controllers/OpenSourceComponentsTableViewController.swift +++ b/pass/Controllers/OpenSourceComponentsTableViewController.swift @@ -44,7 +44,6 @@ class OpenSourceComponentsTableViewController: BasicStaticTableViewController { [CellDataKey.title: item[0], CellDataKey.action: "link", CellDataKey.link: item[1], CellDataKey.accessoryType: UITableViewCellAccessoryType.detailDisclosureButton, CellDataKey.detailDisclosureAction: #selector(actOnDetailDisclosureButton(_:)), CellDataKey.detailDisclosureData: item[2]] ) } - navigationItemTitle = "Open Source Components" super.viewDidLoad() } diff --git a/pass/Controllers/PGPKeyArmorSettingTableViewController.swift b/pass/Controllers/PGPKeyArmorSettingTableViewController.swift index 16ba52b..34f7649 100644 --- a/pass/Controllers/PGPKeyArmorSettingTableViewController.swift +++ b/pass/Controllers/PGPKeyArmorSettingTableViewController.swift @@ -9,12 +9,14 @@ import UIKit import SwiftyUserDefaults -class PGPKeyArmorSettingTableViewController: UITableViewController { +class PGPKeyArmorSettingTableViewController: UITableViewController, UITextViewDelegate { @IBOutlet weak var armorPublicKeyTextView: UITextView! @IBOutlet weak var armorPrivateKeyTextView: UITextView! var pgpPassphrase: String? let passwordStore = PasswordStore.shared + private var recentPastedText = "" + override func viewDidLoad() { super.viewDidLoad() armorPublicKeyTextView.text = Defaults[.pgpPublicKeyArmor] @@ -22,7 +24,7 @@ class PGPKeyArmorSettingTableViewController: UITableViewController { pgpPassphrase = passwordStore.pgpKeyPassphrase } - private func createSavePassphraseAlert() -> UIAlertController { + private func createSavePassphraseAndSegueAlert() -> UIAlertController { let savePassphraseAlert = UIAlertController(title: "Passphrase", message: "Do you want to save the passphrase for later decryption?", preferredStyle: UIAlertControllerStyle.alert) savePassphraseAlert.addAction(UIAlertAction(title: "No", style: UIAlertActionStyle.default) { _ in Defaults[.isRememberPassphraseOn] = false @@ -35,12 +37,26 @@ class PGPKeyArmorSettingTableViewController: UITableViewController { return savePassphraseAlert } + override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { + if identifier == "savePGPKeySegue" { + if armorPublicKeyTextView.text.isEmpty { + Utils.alert(title: "Cannot Save", message: "Please set public key first.", controller: self, completion: nil) + return false + } + if armorPrivateKeyTextView.text.isEmpty { + Utils.alert(title: "Cannot Save", message: "Please set private key first.", controller: self, completion: nil) + return false + } + } + return true + } + @IBAction func save(_ sender: Any) { let alert = UIAlertController(title: "Passphrase", message: "Please fill in the passphrase of your PGP secret key.", preferredStyle: UIAlertControllerStyle.alert) alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: {_ in self.pgpPassphrase = alert.textFields?.first?.text - let savePassphraseAlert = self.createSavePassphraseAlert() - self.present(savePassphraseAlert, animated: true, completion: nil) + let savePassphraseAndSegueAlert = self.createSavePassphraseAndSegueAlert() + self.present(savePassphraseAndSegueAlert, animated: true, completion: nil) })) alert.addTextField(configurationHandler: {(textField: UITextField!) in textField.text = self.pgpPassphrase @@ -48,5 +64,18 @@ class PGPKeyArmorSettingTableViewController: UITableViewController { }) self.present(alert, animated: true, completion: nil) } - + + func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + if text == UIPasteboard.general.string { + // user pastes something, get ready to clear in 10s + recentPastedText = text + DispatchQueue.global(qos: .background).asyncAfter(deadline: DispatchTime.now() + 10) { [weak weakSelf = self] in + if let pasteboardString = UIPasteboard.general.string, + pasteboardString == weakSelf?.recentPastedText { + UIPasteboard.general.string = "" + } + } + } + return true + } } diff --git a/pass/Controllers/PGPKeySettingTableViewController.swift b/pass/Controllers/PGPKeySettingTableViewController.swift index 6423b1e..e99222f 100644 --- a/pass/Controllers/PGPKeySettingTableViewController.swift +++ b/pass/Controllers/PGPKeySettingTableViewController.swift @@ -24,6 +24,19 @@ class PGPKeySettingTableViewController: UITableViewController { pgpPassphrase = passwordStore.pgpKeyPassphrase } + private func createSavePassphraseAndSegueAlert() -> UIAlertController { + let savePassphraseAlert = UIAlertController(title: "Passphrase", message: "Do you want to save the passphrase for later decryption?", preferredStyle: UIAlertControllerStyle.alert) + savePassphraseAlert.addAction(UIAlertAction(title: "No", style: UIAlertActionStyle.default) { _ in + Defaults[.isRememberPassphraseOn] = false + self.performSegue(withIdentifier: "savePGPKeySegue", sender: self) + }) + savePassphraseAlert.addAction(UIAlertAction(title: "Save", style: UIAlertActionStyle.destructive) {_ in + Defaults[.isRememberPassphraseOn] = true + self.performSegue(withIdentifier: "savePGPKeySegue", sender: self) + }) + return savePassphraseAlert + } + override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { if identifier == "savePGPKeySegue" { guard let pgpPublicKeyURL = URL(string: pgpPublicKeyURLTextField.text!) else { @@ -46,9 +59,8 @@ class PGPKeySettingTableViewController: UITableViewController { let alert = UIAlertController(title: "Passphrase", message: "Please fill in the passphrase of your PGP secret key.", preferredStyle: UIAlertControllerStyle.alert) alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: {_ in self.pgpPassphrase = alert.textFields?.first?.text - if self.shouldPerformSegue(withIdentifier: "savePGPKeySegue", sender: self) { - self.performSegue(withIdentifier: "savePGPKeySegue", sender: self) - } + let savePassphraseAndSegueAlert = self.createSavePassphraseAndSegueAlert() + self.present(savePassphraseAndSegueAlert, animated: true, completion: nil) })) alert.addTextField(configurationHandler: {(textField: UITextField!) in textField.text = self.pgpPassphrase diff --git a/pass/Controllers/PasswordDetailTableViewController.swift b/pass/Controllers/PasswordDetailTableViewController.swift index b5590b9..84c5773 100644 --- a/pass/Controllers/PasswordDetailTableViewController.swift +++ b/pass/Controllers/PasswordDetailTableViewController.swift @@ -386,11 +386,8 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni return; } - // increase HOTP counter - password!.increaseHotpCounter() - - // copy HOTP to pasteboard - if let plainPassword = password!.getOtp() { + // copy HOTP to pasteboard (will update counter) + if let plainPassword = password!.getNextHotp() { Utils.copyToPasteboard(textToCopy: plainPassword) } @@ -448,9 +445,6 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni let titleData = tableDataItem.title let contentData = tableDataItem.content cell.delegatePasswordTableView = self - cell.isPasswordCell = (titleData.lowercased() == "password" ? true : false) - cell.isURLCell = (titleData.lowercased() == "url" ? true : false) - cell.isHOTPCell = (titleData == "HMAC-based" ? true : false) cell.cellData = LabelTableViewCellData(title: titleData, content: contentData) cell.selectionStyle = .none return cell diff --git a/pass/Controllers/PasswordEditorTableViewController.swift b/pass/Controllers/PasswordEditorTableViewController.swift index afa1773..c750f7b 100644 --- a/pass/Controllers/PasswordEditorTableViewController.swift +++ b/pass/Controllers/PasswordEditorTableViewController.swift @@ -8,16 +8,17 @@ import UIKit import SwiftyUserDefaults +import OneTimePassword enum PasswordEditorCellType { - case textFieldCell, textViewCell, fillPasswordCell, passwordLengthCell, deletePasswordCell + case textFieldCell, textViewCell, fillPasswordCell, passwordLengthCell, deletePasswordCell, scanQRCodeCell } enum PasswordEditorCellKey { case type, title, content, placeholders } -class PasswordEditorTableViewController: UITableViewController, FillPasswordTableViewCellDelegate, PasswordSettingSliderTableViewCellDelegate, UIGestureRecognizerDelegate { +class PasswordEditorTableViewController: UITableViewController, FillPasswordTableViewCellDelegate, PasswordSettingSliderTableViewCellDelegate, QRScannerControllerDelegate { var tableData = [ [Dictionary] @@ -29,11 +30,13 @@ class PasswordEditorTableViewController: UITableViewController, FillPasswordTabl private var sectionHeaderTitles = ["name", "password", "additions",""].map {$0.uppercased()} private var sectionFooterTitles = ["", "", "Use \"key: value\" format for additional fields.", ""] private let passwordSection = 1 + private let additionsSection = 2 private var hidePasswordSettings = true private var fillPasswordCell: FillPasswordTableViewCell? private var passwordLengthCell: SliderTableViewCell? private var deletePasswordCell: UITableViewCell? + private var scanQRCodeCell: UITableViewCell? override func loadView() { super.loadView() @@ -42,11 +45,22 @@ class PasswordEditorTableViewController: UITableViewController, FillPasswordTabl deletePasswordCell!.textLabel?.text = "Delete Password" deletePasswordCell!.textLabel?.textColor = Globals.red deletePasswordCell?.selectionStyle = .default + + scanQRCodeCell = UITableViewCell(style: .default, reuseIdentifier: "default") + scanQRCodeCell?.textLabel?.text = "Add One-Time Password" + scanQRCodeCell?.textLabel?.textColor = Globals.blue + scanQRCodeCell?.selectionStyle = .default +// scanQRCodeCell?.imageView?.image = #imageLiteral(resourceName: "Camera").withRenderingMode(.alwaysTemplate) +// scanQRCodeCell?.imageView?.tintColor = Globals.blue +// scanQRCodeCell?.imageView?.contentMode = .scaleAspectFit } override func viewDidLoad() { super.viewDidLoad() - navigationItem.title = navigationItemTitle + if navigationItemTitle != nil { + navigationItem.title = navigationItemTitle + } + tableView.register(UINib(nibName: "TextFieldTableViewCell", bundle: nil), forCellReuseIdentifier: "textFieldCell") tableView.register(UINib(nibName: "TextViewTableViewCell", bundle: nil), forCellReuseIdentifier: "textViewCell") tableView.register(UINib(nibName: "FillPasswordTableViewCell", bundle: nil), forCellReuseIdentifier: "fillPasswordCell") @@ -83,6 +97,8 @@ class PasswordEditorTableViewController: UITableViewController, FillPasswordTabl return passwordLengthCell! case .deletePasswordCell: return deletePasswordCell! + case .scanQRCodeCell: + return scanQRCodeCell! default: let cell = tableView.dequeueReusableCell(withIdentifier: "textFieldCell", for: indexPath) as! ContentTableViewCell cell.setContent(content: cellData[PasswordEditorCellKey.content] as? String) @@ -116,13 +132,16 @@ class PasswordEditorTableViewController: UITableViewController, FillPasswordTabl } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if tableView.cellForRow(at: indexPath) == deletePasswordCell { + let selectedCell = tableView.cellForRow(at: indexPath) + if selectedCell == deletePasswordCell { let alert = UIAlertController(title: "Delete Password?", message: nil, preferredStyle: UIAlertControllerStyle.alert) alert.addAction(UIAlertAction(title: "Delete", style: UIAlertActionStyle.destructive, handler: {[unowned self] (action) -> Void in self.performSegue(withIdentifier: "deletePasswordSegue", sender: self) })) alert.addAction(UIAlertAction(title: "Cancel", style: UIAlertActionStyle.cancel, handler:nil)) self.present(alert, animated: true, completion: nil) + } else if selectedCell == scanQRCodeCell { + self.performSegue(withIdentifier: "showQRScannerSegue", sender: self) } tableView.deselectRow(at: indexPath, animated: true) } @@ -165,4 +184,53 @@ class PasswordEditorTableViewController: UITableViewController, FillPasswordTabl hidePasswordSettings = !hidePasswordSettings tableView.reloadSections([passwordSection], with: .fade) } + + func insertScannedOTPFields(_ otpauth: String) { + // update tableData + if let additionsPlainText = (tableData[additionsSection][0][PasswordEditorCellKey.content] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines), additionsPlainText != "" { + tableData[additionsSection][0][PasswordEditorCellKey.content] = additionsPlainText + "\n" + otpauth + } else { + tableData[additionsSection][0][PasswordEditorCellKey.content] = otpauth + } + // reload + tableView.reloadSections([additionsSection], with: .none) + } + + // MARK: - QRScannerControllerDelegate Methods + func checkScannedOutput(line: String) -> (accept: Bool, message: String) { + if let url = URL(string: line), let _ = Token(url: url) { + return (accept: true, message: "Valid token URL") + } else { + return (accept: false, message: "Invalid token URL") + } + } + + // MARK: - QRScannerControllerDelegate Methods + func handleScannedOutput(line: String) { + insertScannedOTPFields(line) + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == "showQRScannerSegue" { + if let navController = segue.destination as? UINavigationController { + if let viewController = navController.topViewController as? QRScannerController { + viewController.delegate = self + } + } else if let viewController = segue.destination as? QRScannerController { + viewController.delegate = self + } + } + } + + @IBAction private func cancelOTPScanner(segue: UIStoryboardSegue) { + + } + + @IBAction func saveScannedOTP(segue: UIStoryboardSegue) { + if let controller = segue.source as? OTPScannerController { + if let scannedOTP = controller.scannedOTP { + insertScannedOTPFields(scannedOTP) + } + } + } } diff --git a/pass/Controllers/PasswordsViewController.swift b/pass/Controllers/PasswordsViewController.swift index defb6ec..c4bc68e 100644 --- a/pass/Controllers/PasswordsViewController.swift +++ b/pass/Controllers/PasswordsViewController.swift @@ -11,25 +11,28 @@ import SVProgressHUD import SwiftyUserDefaults import PasscodeLock -enum PasswordsTableEntryType { - case password, dir -} - -struct PasswordsTableEntry { +fileprivate class PasswordsTableEntry : NSObject { var title: String var isDir: Bool var passwordEntity: PasswordEntity? + init(title: String, isDir: Bool, passwordEntity: PasswordEntity?) { + self.title = title + self.isDir = isDir + self.passwordEntity = passwordEntity + } } -class PasswordsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UITabBarControllerDelegate { +class PasswordsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UITabBarControllerDelegate, UISearchBarDelegate { private var passwordsTableEntries: [PasswordsTableEntry] = [] + private var passwordsTableAllEntries: [PasswordsTableEntry] = [] private var filteredPasswordsTableEntries: [PasswordsTableEntry] = [] private var parentPasswordEntity: PasswordEntity? = nil private let passwordStore = PasswordStore.shared private var tapTabBarTime: TimeInterval = 0 - private var sections : [(index: Int, length :Int, title: String)] = Array() + private var sections = [(title: String, entries: [PasswordsTableEntry])]() + private var searchActive : Bool = false private lazy var searchController: UISearchController = { @@ -80,8 +83,10 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV private func initPasswordsTableEntries(parent: PasswordEntity?) { passwordsTableEntries.removeAll() + passwordsTableAllEntries.removeAll() filteredPasswordsTableEntries.removeAll() var passwordEntities = [PasswordEntity]() + var passwordAllEntities = [PasswordEntity]() if Defaults[.isShowFolderOn] { passwordEntities = self.passwordStore.fetchPasswordEntityCoreData(parent: parent) } else { @@ -90,6 +95,10 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV passwordsTableEntries = passwordEntities.map { PasswordsTableEntry(title: $0.name!, isDir: $0.isDir, passwordEntity: $0) } + passwordAllEntities = self.passwordStore.fetchPasswordEntityCoreData(withDir: false) + passwordsTableAllEntries = passwordAllEntities.map { + PasswordsTableEntry(title: $0.name!, isDir: $0.isDir, passwordEntity: $0) + } parentPasswordEntity = parent } @@ -149,17 +158,31 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV } } catch { DispatchQueue.main.async { - Utils.alert(title: "Error", message: error.localizedDescription, controller: self, completion: nil) + SVProgressHUD.dismiss() + self.syncControl.endRefreshing() + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(800)) { + Utils.alert(title: "Error", message: error.localizedDescription, controller: self, completion: nil) + } } } } } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if Defaults[.isShowFolderOn] { + searchController.searchBar.scopeButtonTitles = ["Current", "All"] + } else { + searchController.searchBar.scopeButtonTitles = nil + } + } override func viewDidLoad() { super.viewDidLoad() tabBarController!.delegate = self + searchController.searchBar.delegate = self tableView.delegate = self tableView.dataSource = self definesPresentationContext = true @@ -196,13 +219,13 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return sections[section].length + return sections[section].entries.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressAction(_:))) longPressGestureRecognizer.minimumPressDuration = 0.6 - if Defaults[.isShowFolderOn] { + if Defaults[.isShowFolderOn] && searchController.searchBar.selectedScopeButtonIndex == 0{ let cell = tableView.dequeueReusableCell(withIdentifier: "passwordTableViewCell", for: indexPath) let entry = getPasswordEntry(by: indexPath) @@ -241,14 +264,7 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV } private func getPasswordEntry(by indexPath: IndexPath) -> PasswordsTableEntry { - var entry: PasswordsTableEntry - let index = sections[indexPath.section].index + indexPath.row - if searchController.isActive && searchController.searchBar.text != "" { - entry = filteredPasswordsTableEntries[index] - } else { - entry = passwordsTableEntries[index] - } - return entry + return sections[indexPath.section].entries[indexPath.row] } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { @@ -268,7 +284,11 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV func backAction(_ sender: Any?) { guard Defaults[.isShowFolderOn] else { return } - reloadTableView(parent: parentPasswordEntity?.parent, anim: transitionFromLeft) + var anim: CATransition? = transitionFromLeft + if parentPasswordEntity == nil { + anim = nil + } + reloadTableView(parent: parentPasswordEntity?.parent, anim: anim) } func longPressAction(_ gesture: UILongPressGestureRecognizer) { @@ -345,25 +365,30 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV } private func generateSections(item: [PasswordsTableEntry]) { - sections.removeAll() - guard item.count != 0 else { - return + let collation = UILocalizedIndexedCollation.current() + let sectionTitles = collation.sectionIndexTitles + var newSections = [(title: String, entries: [PasswordsTableEntry])]() + + // initialize all sections + for i in 0.. 0} } func actOnSearchNotification() { @@ -381,6 +406,11 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV } return false } + } else if identifier == "addPasswordSegue" { + guard self.passwordStore.publicKey != nil, self.passwordStore.storeRepository != nil else { + Utils.alert(title: "Cannot Add Password", message: "Please make sure PGP Key and Git Server are properly set.", controller: self, completion: nil) + return false + } } return true } @@ -396,14 +426,30 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV } func filterContentForSearchText(searchText: String, scope: String = "All") { - filteredPasswordsTableEntries = passwordsTableEntries.filter { entry in - return entry.title.lowercased().contains(searchText.lowercased()) - } - if searchController.isActive && searchController.searchBar.text != "" { - reloadTableView(data: filteredPasswordsTableEntries) - } else { - reloadTableView(data: passwordsTableEntries) + switch scope { + case "All": + filteredPasswordsTableEntries = passwordsTableAllEntries.filter { entry in + return entry.title.lowercased().contains(searchText.lowercased()) + } + if searchController.isActive && searchController.searchBar.text != "" { + reloadTableView(data: filteredPasswordsTableEntries) + } else { + reloadTableView(data: passwordsTableAllEntries) + } + case "Current": + filteredPasswordsTableEntries = passwordsTableEntries.filter { entry in + return entry.title.lowercased().contains(searchText.lowercased()) + } + if searchController.isActive && searchController.searchBar.text != "" { + reloadTableView(data: filteredPasswordsTableEntries) + } else { + reloadTableView(data: passwordsTableEntries) + } + default: + break } + + } private func reloadTableView(data: [PasswordsTableEntry], anim: CAAnimation? = nil) { @@ -466,10 +512,22 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV backAction(self) } } + func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) { + updateSearchResults(for: searchController) + } + + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + searchController.searchBar.selectedScopeButtonIndex = 0 + updateSearchResults(for: searchController) + } } extension PasswordsViewController: UISearchResultsUpdating { func updateSearchResults(for searchController: UISearchController) { - filterContentForSearchText(searchText: searchController.searchBar.text!) + var scope = "All" + if let scopeButtonTitles = searchController.searchBar.scopeButtonTitles { + scope = scopeButtonTitles[searchController.searchBar.selectedScopeButtonIndex] + } + filterContentForSearchText(searchText: searchController.searchBar.text!, scope: scope) } } diff --git a/pass/Controllers/QRScannerController.swift b/pass/Controllers/QRScannerController.swift new file mode 100644 index 0000000..13e40da --- /dev/null +++ b/pass/Controllers/QRScannerController.swift @@ -0,0 +1,128 @@ +// +// QRScannerController.swift +// pass +// +// Created by Yishi Lin on 7/4/17. +// Copyright © 2017 Yishi Lin. All rights reserved. +// + +import UIKit +import AVFoundation +import OneTimePassword +import SVProgressHUD + +protocol QRScannerControllerDelegate { + func checkScannedOutput(line: String) -> (accept: Bool, message: String) + func handleScannedOutput(line: String) +} + +class QRScannerController: UIViewController, AVCaptureMetadataOutputObjectsDelegate { + + @IBOutlet weak var scannerOutput: UILabel! + + var captureSession: AVCaptureSession? + var videoPreviewLayer: AVCaptureVideoPreviewLayer? + var qrCodeFrameView: UIView? + + let supportedCodeTypes = [AVMetadataObjectTypeQRCode] + + var delegate: QRScannerControllerDelegate? + + override func viewDidLoad() { + super.viewDidLoad() + + // Get an instance of the AVCaptureDevice class to initialize a device object and provide the video as the media type parameter. + let captureDevice = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo) + + do { + // Get an instance of the AVCaptureDeviceInput class using the previous device object. + let input = try AVCaptureDeviceInput(device: captureDevice) + + // Initialize the captureSession object. + captureSession = AVCaptureSession() + + // Set the input device on the capture session. + captureSession?.addInput(input) + + // Initialize a AVCaptureMetadataOutput object and set it as the output device to the capture session. + let captureMetadataOutput = AVCaptureMetadataOutput() + captureSession?.addOutput(captureMetadataOutput) + + // Set delegate and use the default dispatch queue to execute the call back + captureMetadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) + captureMetadataOutput.metadataObjectTypes = supportedCodeTypes + + // Initialize the video preview layer and add it as a sublayer to the viewPreview view's layer. + videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession) + videoPreviewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill + videoPreviewLayer?.frame = view.layer.bounds + view.layer.addSublayer(videoPreviewLayer!) + + // Start video capture. + captureSession?.startRunning() + + // Move the message label to the front + scannerOutput.layer.cornerRadius = 10 + scannerOutput.text = "No QR code detected" + view.bringSubview(toFront: scannerOutput) + + // Initialize QR Code Frame to highlight the QR code + qrCodeFrameView = UIView() + + if let qrCodeFrameView = qrCodeFrameView { + qrCodeFrameView.layer.borderColor = UIColor.green.cgColor + qrCodeFrameView.layer.borderWidth = 2 + view.addSubview(qrCodeFrameView) + view.bringSubview(toFront: qrCodeFrameView) + } + + } catch { + print(error) + return + } + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + + // MARK: - AVCaptureMetadataOutputObjectsDelegate Methods + + func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [Any]!, from connection: AVCaptureConnection!) { + + if let metadataObj = metadataObjects.first as? AVMetadataMachineReadableCodeObject, + supportedCodeTypes.contains(metadataObj.type), + let barCodeObject = videoPreviewLayer?.transformedMetadataObject(for: metadataObj) { + + // draw a bounds on the found QR code + qrCodeFrameView?.frame = barCodeObject.bounds + + // check whether it is a valid result + if let scanned = metadataObj.stringValue { + if let (accept, message) = delegate?.checkScannedOutput(line: scanned) { + scannerOutput.text = message + if accept == true { + captureSession?.stopRunning() + delegate?.handleScannedOutput(line: scanned) + DispatchQueue.main.async { + SVProgressHUD.showSuccess(withStatus: "Done") + SVProgressHUD.dismiss(withDelay: 1) + self.navigationController?.popViewController(animated: true) + } + } + } else { + // no delegate, show the scanned result + scannerOutput.text = scanned + } + } else { + scannerOutput.text = "No string value" + } + + } else { + qrCodeFrameView?.frame = CGRect.zero + scannerOutput.text = "No QR code detected" + } + } +} diff --git a/pass/Controllers/RawPasswordViewController.swift b/pass/Controllers/RawPasswordViewController.swift index 4b844a2..78d9e98 100644 --- a/pass/Controllers/RawPasswordViewController.swift +++ b/pass/Controllers/RawPasswordViewController.swift @@ -15,7 +15,9 @@ class RawPasswordViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - navigationItem.title = "Raw" + navigationItem.title = password?.name + rawPasswordTextView.textContainer.lineFragmentPadding = 0 + rawPasswordTextView.textContainerInset = .zero rawPasswordTextView.text = password?.plainText } } diff --git a/pass/Controllers/SSHKeySettingTableViewController.swift b/pass/Controllers/SSHKeySettingTableViewController.swift index eec6800..23f0c5c 100644 --- a/pass/Controllers/SSHKeySettingTableViewController.swift +++ b/pass/Controllers/SSHKeySettingTableViewController.swift @@ -20,17 +20,10 @@ class SSHKeySettingTableViewController: UITableViewController { super.viewDidLoad() privateKeyURLTextField.text = Defaults[.gitSSHPrivateKeyURL]?.absoluteString publicKeyURLTextField.text = Defaults[.gitSSHPublicKeyURL]?.absoluteString - var doneBarButtonItem: UIBarButtonItem? - - doneBarButtonItem = UIBarButtonItem(title: "Done", - style: UIBarButtonItemStyle.done, - target: self, - action: #selector(doneButtonTapped(_:))) - navigationItem.rightBarButtonItem = doneBarButtonItem - navigationItem.title = "SSH Key" } - func doneButtonTapped(_ sender: UIButton) { + + @IBAction func doneButtonTapped(_ sender: UIButton) { guard let publicKeyURL = URL(string: publicKeyURLTextField.text!) else { Utils.alert(title: "Cannot Save", message: "Please set Public Key URL first.", controller: self, completion: nil) return diff --git a/pass/Controllers/SpecialThanksTableViewController.swift b/pass/Controllers/SpecialThanksTableViewController.swift index 3c08198..8d58cee 100644 --- a/pass/Controllers/SpecialThanksTableViewController.swift +++ b/pass/Controllers/SpecialThanksTableViewController.swift @@ -27,7 +27,6 @@ class SpecialThanksTableViewController: BasicStaticTableViewController { [CellDataKey.action: "link", CellDataKey.title: item[0], CellDataKey.link: item[1]] ) } - navigationItemTitle = "Special Thanks" super.viewDidLoad() } } diff --git a/pass/Helpers/Globals.swift b/pass/Helpers/Globals.swift index 13a3e59..0551b04 100644 --- a/pass/Helpers/Globals.swift +++ b/pass/Helpers/Globals.swift @@ -27,6 +27,7 @@ class Globals { "Apple": (min: 15, max: 15, def: 15)] static let passwordDots = "••••••••••••" + static let oneTimePasswordDots = "••••••" static let passwordFonts = "Menlo" // UI related diff --git a/pass/Helpers/UITextFieldExtension.swift b/pass/Helpers/UITextFieldExtension.swift new file mode 100644 index 0000000..2500baf --- /dev/null +++ b/pass/Helpers/UITextFieldExtension.swift @@ -0,0 +1,34 @@ +// +// UIViewControllerExtionsion.swift +// pass +// +// Created by Yishi Lin on 5/4/17. +// Copyright © 2017 Yishi Lin. All rights reserved. +// + +import Foundation +import UIKit + +private var kAssociationKeyNextField: UInt8 = 0 + +extension UITextField { + @IBOutlet var nextField: UITextField? { + get { + return objc_getAssociatedObject(self, &kAssociationKeyNextField) as? UITextField + } + set(newField) { + objc_setAssociatedObject(self, &kAssociationKeyNextField, newField, .OBJC_ASSOCIATION_RETAIN) + } + } +} + +extension UIViewController { + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + if textField.nextField != nil { + textField.nextField?.becomeFirstResponder() + } else { + textField.resignFirstResponder() + } + return true + } +} diff --git a/pass/Helpers/Utils.swift b/pass/Helpers/Utils.swift index 5dcd647..daa4496 100644 --- a/pass/Helpers/Utils.swift +++ b/pass/Helpers/Utils.swift @@ -50,7 +50,7 @@ class Utils { static func randomString(length: Int) -> String { - let letters : NSString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + let letters : NSString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*_+-=" let len = UInt32(letters.length) var randomString = "" diff --git a/pass/Info.plist b/pass/Info.plist index 11cab7f..ebce541 100644 --- a/pass/Info.plist +++ b/pass/Info.plist @@ -17,13 +17,15 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.2.3 + 0.2.4 CFBundleVersion 1 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS + NSCameraUsageDescription + We need to access your camera for scanning QR code. UIApplicationShortcutItems diff --git a/pass/Models/Password.swift b/pass/Models/Password.swift index 2fec51e..dd5106d 100644 --- a/pass/Models/Password.swift +++ b/pass/Models/Password.swift @@ -61,21 +61,20 @@ class Password { private func initEverything(name: String, plainText: String) { self.name = name self.plainText = plainText + self.additions.removeAll() + self.additionKeys.removeAll() // get password and additional fields let plainTextSplit = plainText.characters.split(maxSplits: 1, omittingEmptySubsequences: false) { $0 == "\n" || $0 == "\r\n" }.map(String.init) - guard plainTextSplit.count > 0 else { - return; - } - self.password = plainTextSplit[0] + self.password = plainTextSplit.first ?? "" if plainTextSplit.count == 2 { (self.additions, self.additionKeys) = Password.getAdditionFields(from: plainTextSplit[1]) } // check whether the first line of the plainText looks like an otp entry - let (key, value) = Password.getKeyValuePair(from: plainTextSplit[0]) + let (key, value) = Password.getKeyValuePair(from: self.password) if Password.otpKeywords.contains(key ?? "") { firstLineIsOTPField = true self.additions[key!] = value @@ -99,12 +98,16 @@ class Password { // return a key-value pair from the line // key might be nil, if there is no ":" in the line static private func getKeyValuePair(from line: String) -> (key: String?, value: String) { - let items = line.characters.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false).map{String($0).trimmingCharacters(in: .whitespaces)} + let items = line.components(separatedBy: ": ").map{String($0).trimmingCharacters(in: .whitespaces)} var key : String? = nil var value = "" if items.count == 1 || (items[0].isEmpty && items[1].isEmpty) { - // no ":" found, or empty on both sides of ":" (e.g., " : ") + // no ": " found, or empty on both sides of ": " value = line + // otpauth special case + if value.hasPrefix("otpauth://") { + key = "otpauth" + } } else { if !items[0].isEmpty { key = items[0] @@ -265,21 +268,6 @@ class Password { } } - // it is guaranteed that it is a HOTP password when we call this - func increaseHotpCounter() { - var lines : [String] = [] - self.plainText.enumerateLines() { line, _ in - let (key, value) = Password.getKeyValuePair(from: line) - if key == "otp_counter", let newValue = UInt64(value)?.advanced(by: 1) { - let newLine = "\(key!): \(newValue)" - lines.append(newLine) - } else { - lines.append(line) - } - } - self.updatePassword(name: self.name, plainText: lines.joined(separator: "\n")) - } - // return the description and the password strings func getOtpStrings() -> (description: String, otp: String)? { guard let token = self.otpToken else { @@ -309,6 +297,37 @@ class Password { } } + // return the password strings + // it is guaranteed that it is a HOTP password when we call this + func getNextHotp() -> String? { + // increase the counter + otpToken = otpToken?.updatedToken() + + // replace old HOTP settings with the new otpauth + var newOtpauth = try! otpToken?.toURL().absoluteString + newOtpauth?.append("&secret=") + newOtpauth?.append(MF_Base32Codec.base32String(from: otpToken?.generator.secret)) + + var lines : [String] = [] + self.plainText.enumerateLines() { line, _ in + let (key, _) = Password.getKeyValuePair(from: line) + if !Password.otpKeywords.contains(key ?? "") { + lines.append(line) + } else if key == "otpauth" && newOtpauth != nil { + lines.append(newOtpauth!) + // set to nil to prevent duplication + newOtpauth = nil + } + } + if newOtpauth != nil { + lines.append(newOtpauth!) + } + self.updatePassword(name: self.name, plainText: lines.joined(separator: "\n")) + + // get and return the password + return self.otpToken?.currentPassword + } + static func LooksLikeOTP(line: String) -> Bool { let (key, _) = getKeyValuePair(from: line) return Password.otpKeywords.contains(key ?? "") diff --git a/pass/Models/PasswordStore.swift b/pass/Models/PasswordStore.swift index ee782ef..ff05a3b 100644 --- a/pass/Models/PasswordStore.swift +++ b/pass/Models/PasswordStore.swift @@ -220,27 +220,21 @@ class PasswordStore { } public func initPGPKey(_ keyType: PGPKeyType) throws { - var keyPath = "" switch keyType { case .public: - keyPath = Globals.pgpPublicKeyPath - case .secret: - keyPath = Globals.pgpPrivateKeyPath - default: - throw NSError(domain: "me.mssun.pass.error", code: 2, userInfo: [NSLocalizedDescriptionKey: "Cannot import key."]) - } - - if let key = importKey(from: keyPath) { - switch keyType { - case .public: - self.publicKey = key - case .secret: - self.privateKey = key - default: - throw NSError(domain: "me.mssun.pass.error", code: 2, userInfo: [NSLocalizedDescriptionKey: "Cannot import key."]) + let keyPath = Globals.pgpPublicKeyPath + self.publicKey = importKey(from: keyPath) + if self.publicKey == nil { + throw NSError(domain: "me.mssun.pass.error", code: 2, userInfo: [NSLocalizedDescriptionKey: "Cannot import the public PGP key."]) } - } else { - throw NSError(domain: "me.mssun.pass.error", code: 2, userInfo: [NSLocalizedDescriptionKey: "Cannot import key."]) + case .secret: + let keyPath = Globals.pgpPrivateKeyPath + self.privateKey = importKey(from: keyPath) + if self.privateKey == nil { + throw NSError(domain: "me.mssun.pass.error", code: 2, userInfo: [NSLocalizedDescriptionKey: "Cannot import the private PGP key."]) + } + default: + throw NSError(domain: "me.mssun.pass.error", code: 2, userInfo: [NSLocalizedDescriptionKey: "Cannot import key: unknown PGP key type."]) } } @@ -272,9 +266,7 @@ class PasswordStore { let fm = FileManager.default if fm.fileExists(atPath: keyPath) { if let keys = pgp.importKeys(fromFile: keyPath, allowDuplicates: false) as? [PGPKey] { - if keys.count > 0 { - return keys[0] - } + return keys.first } } return nil diff --git a/pass/Views/FillPasswordTableViewCell.xib b/pass/Views/FillPasswordTableViewCell.xib index a3c501d..e78839d 100644 --- a/pass/Views/FillPasswordTableViewCell.xib +++ b/pass/Views/FillPasswordTableViewCell.xib @@ -1,26 +1,26 @@ - + - + - + - + - + @@ -32,7 +32,7 @@