Merge branch 'release/0.2.2'

This commit is contained in:
Bob Sun 2017-03-24 10:06:04 -07:00
commit 02fe05c32f
No known key found for this signature in database
GPG key ID: 1F86BA2052FED3B4
32 changed files with 664 additions and 400 deletions

View file

@ -14,21 +14,24 @@ testing. Thank you.
## Features
- Try to be compatible with Password Store command line tool
- Try to be compatible with the Password Store command line tool
- Support to view, copy, add, edit password entries
- Encrypt and decrypt password entries by PGP keys
- Synchronize with you password Git repository
- Synchronize with your password Git repository
- User-friendly interface: search, long press to copy, copy and open link, etc.
- Support one-time password (OTP) tokens
- Written in Swift
- No need to jailbreak your devices
- Get from App Store (stay tuned, under review)
- Get from App Store (stay tuned)
## Screenshots
<p>
<img src="screenshot/preview.gif" width="200"/>
<img src="screenshot/screenshot1.png" width="200"/>
<img src="screenshot/screenshot2.png" width="200"/>
<img src="screenshot/screenshot3.png" width="200"/>
</p>
## Build

View file

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12106.1" systemVersion="16E183b" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="YoR-iB-XAd">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11762" systemVersion="16D32" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="YoR-iB-XAd">
<device id="retina5_5" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12074.1"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@ -288,20 +288,20 @@
<rect key="frame" x="0.0" y="28" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="OfB-1N-1Am" id="fh0-au-C6q">
<rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" tag="101" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="00/00/00" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hj4-EP-LFW">
<rect key="frame" x="15" y="8" width="67" height="28"/>
<label opaque="NO" userInteractionEnabled="NO" tag="101" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="00/00/0000" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hj4-EP-LFW">
<rect key="frame" x="15" y="8" width="89" height="28"/>
<constraints>
<constraint firstAttribute="width" constant="67" id="jNO-Xy-H99"/>
<constraint firstAttribute="width" constant="89" id="jNO-Xy-H99"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<color key="textColor" red="0.5568627451" green="0.5568627451" blue="0.57647058819999997" alpha="1" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" tag="102" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Edit password for totp-secret/github.com using vi." textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="VFa-a4-XsP">
<rect key="frame" x="82" y="12.666666666666664" width="324" height="18"/>
<rect key="frame" x="104" y="12.666666666666664" width="302" height="18"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@ -340,10 +340,10 @@
<tableViewSection headerTitle="Git Repository URL" id="pbe-W6-w4V">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" reuseIdentifier="gitRepositoryURLTabelViewCell" rowHeight="52" id="FRr-pf-aPO">
<rect key="frame" x="0.0" y="55.5" width="414" height="52"/>
<rect key="frame" x="0.0" y="55" width="414" height="52"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="FRr-pf-aPO" id="60A-PS-qGe">
<rect key="frame" x="0.0" y="0.0" width="414" height="52"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Git Repository URL" textAlignment="natural" minimumFontSize="17" clearButtonMode="whileEditing" translatesAutoresizingMaskIntoConstraints="NO" id="EVT-VU-sCi">
@ -366,10 +366,10 @@
<tableViewSection headerTitle="Username" id="fRu-A2-SCk">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" reuseIdentifier="usernameTableVIewCell" rowHeight="52" id="tnj-5U-kMB">
<rect key="frame" x="0.0" y="163.5" width="414" height="52"/>
<rect key="frame" x="0.0" y="163" width="414" height="52"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="tnj-5U-kMB" id="f0c-pI-MSJ">
<rect key="frame" x="0.0" y="0.0" width="414" height="52"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Username" textAlignment="natural" minimumFontSize="17" clearButtonMode="whileEditing" translatesAutoresizingMaskIntoConstraints="NO" id="TMg-Gk-7nG">
@ -392,10 +392,10 @@
<tableViewSection headerTitle="Authentication Method" id="h0N-tI-shZ">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="2" indentationWidth="0.0" shouldIndentWhileEditing="NO" id="KrP-nb-haa">
<rect key="frame" x="0.0" y="271.5" width="414" height="44"/>
<rect key="frame" x="0.0" y="271" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KrP-nb-haa" id="1uB-oE-kfI">
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="43"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Password" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="LfQ-Af-j2O">
@ -423,10 +423,10 @@
<inset key="separatorInset" minX="62" minY="0.0" maxX="0.0" maxY="0.0"/>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="blue" accessoryType="detailButton" hidesAccessoryWhenEditing="NO" indentationLevel="2" indentationWidth="0.0" shouldIndentWhileEditing="NO" id="Qmt-bo-CuJ">
<rect key="frame" x="0.0" y="315.5" width="414" height="44"/>
<rect key="frame" x="0.0" y="315" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Qmt-bo-CuJ" id="p3u-8b-h3U">
<rect key="frame" x="0.0" y="0.0" width="367" height="44"/>
<rect key="frame" x="0.0" y="0.0" width="367" height="43"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SSH Key" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ezz-76-a53">
@ -503,7 +503,7 @@
<rect key="frame" x="0.0" y="35" width="414" height="52"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="BYZ-9g-xZy" id="Zfn-rK-sN1">
<rect key="frame" x="0.0" y="0.0" width="414" height="51.5"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="52"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Public Key URL" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dWi-eh-7Eq">
@ -533,7 +533,7 @@
<rect key="frame" x="0.0" y="87" width="414" height="52"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="vpk-J8-j7t" id="1td-qT-6ts">
<rect key="frame" x="0.0" y="0.0" width="414" height="51.5"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="52"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Private Key URL" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Qht-RC-Yeg">
@ -727,6 +727,9 @@
</connections>
</barButtonItem>
</navigationItem>
<connections>
<segue destination="HB6-Yu-Y3J" kind="unwind" identifier="deletePasswordSegue" unwindAction="deletePasswordWithSegue:" id="L1Z-64-EZh"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="HlX-6r-eOU" userLabel="First Responder" sceneMemberID="firstResponder"/>
<exit id="HB6-Yu-Y3J" userLabel="Exit" sceneMemberID="exit"/>
@ -788,7 +791,7 @@
<rect key="frame" x="0.0" y="35" width="414" height="52"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Jy1-4S-Lvf" id="tJE-ww-okf">
<rect key="frame" x="0.0" y="0.0" width="414" height="51.5"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="52"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Public Key URL" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Oys-xP-ZrB">
@ -818,7 +821,7 @@
<rect key="frame" x="0.0" y="87" width="414" height="52"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="MA5-lE-8dT" id="pTv-Wj-psC">
<rect key="frame" x="0.0" y="0.0" width="414" height="51.5"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="52"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Private Key URL" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="C2w-dd-roS">
@ -848,7 +851,7 @@
<rect key="frame" x="0.0" y="139" width="414" height="52"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="yER-vT-YTO" id="Mip-zw-xLu">
<rect key="frame" x="0.0" y="0.0" width="414" height="51.5"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="52"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Passphrase" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Rnj-2x-ksO">
@ -950,7 +953,28 @@
<rect key="frame" x="0.0" y="0.0" width="414" height="43.666666666666664"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Discard All Local Changes" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Jwg-mt-woS">
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Encrypt in ASCII-Armored" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Jwg-mt-woS">
<rect key="frame" x="15" y="0.0" width="384" height="43.666666666666664"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection id="aVR-FE-jMg">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" textLabel="zrl-v3-fxg" style="IBUITableViewCellStyleDefault" id="Jm8-B5-wKx" userLabel="Discard Changes Table View Cell">
<rect key="frame" x="0.0" y="115" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Jm8-B5-wKx" id="tjS-Q6-y2M">
<rect key="frame" x="0.0" y="0.0" width="414" height="43.666666666666664"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Discard All Local Changes" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="zrl-v3-fxg">
<rect key="frame" x="15" y="0.0" width="384" height="43.666666666666664"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
@ -965,7 +989,7 @@
<tableViewSection id="ujw-Wl-vs1">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" textLabel="K2K-Bx-g7Z" style="IBUITableViewCellStyleDefault" id="NI1-Kd-hyH">
<rect key="frame" x="0.0" y="115" width="414" height="44"/>
<rect key="frame" x="0.0" y="195" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="NI1-Kd-hyH" id="yLe-T2-TWF">
<rect key="frame" x="0.0" y="0.0" width="414" height="43.666666666666664"/>
@ -990,7 +1014,8 @@
</connections>
</tableView>
<connections>
<outlet property="discardChangesTableViewCell" destination="tHt-Ro-0HF" id="E7G-0v-MBB"/>
<outlet property="discardChangesTableViewCell" destination="Jm8-B5-wKx" id="rfA-G3-2OE"/>
<outlet property="encryptInASCIIArmoredTableViewCell" destination="tHt-Ro-0HF" id="tOi-Sj-mLJ"/>
<outlet property="eraseDataTableViewCell" destination="NI1-Kd-hyH" id="NtJ-f4-oxb"/>
</connections>
</tableViewController>
@ -1110,7 +1135,7 @@
<rect key="frame" x="0.0" y="35" width="414" height="170"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Pv0-ev-stj" id="ywz-II-W1g">
<rect key="frame" x="0.0" y="0.0" width="414" height="169.5"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="170"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" bounces="NO" scrollEnabled="NO" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" delaysContentTouches="NO" canCancelContentTouches="NO" bouncesZoom="NO" editable="NO" usesAttributedText="YES" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xzQ-5d-kdL">
@ -1168,7 +1193,7 @@
<rect key="frame" x="0.0" y="261" width="414" height="160"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Lom-iT-l16" id="eya-Tv-r0q">
<rect key="frame" x="0.0" y="0.0" width="414" height="159.5"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="160"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="oyB-oI-1fS">
@ -1224,7 +1249,7 @@ pfZ36xQbOAQYKKf6ZTT5R/Y=
<rect key="frame" x="0.0" y="477" width="414" height="160"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="J8U-ev-FRQ" id="eb0-vb-Fcc">
<rect key="frame" x="0.0" y="0.0" width="414" height="159.5"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="160"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="lrQ-Ln-ZOv">

View file

@ -10,26 +10,19 @@ import UIKit
class AboutRepositoryTableViewController: BasicStaticTableViewController {
var needRefresh = false
var indicatorLabel: UILabel!
var indicator: UIActivityIndicatorView!
let passwordStore = PasswordStore.shared
private var needRefresh = false
private var indicator: UIActivityIndicatorView = {
let indicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
return indicator
}()
private let passwordStore = PasswordStore.shared
override func viewDidLoad() {
navigationItemTitle = "About Repository"
super.viewDidLoad()
indicatorLabel = UILabel(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 21))
indicatorLabel.center = CGPoint(x: view.frame.size.width / 2, y: view.frame.size.height * 0.382 + 22)
indicatorLabel.backgroundColor = UIColor.clear
indicatorLabel.textColor = UIColor.gray
indicatorLabel.text = "calculating"
indicatorLabel.textAlignment = .center
indicatorLabel.font = UIFont.preferredFont(forTextStyle: .footnote)
indicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
indicator.center = CGPoint(x: view.frame.size.width / 2, y: view.frame.size.height * 0.382)
indicator.center = CGPoint(x: view.bounds.midX, y: view.bounds.height * 0.382)
tableView.addSubview(indicator)
tableView.addSubview(indicatorLabel)
setTableData()
@ -40,7 +33,6 @@ class AboutRepositoryTableViewController: BasicStaticTableViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if needRefresh {
indicatorLabel.text = "reloading"
setTableData()
needRefresh = false
}
@ -51,45 +43,32 @@ class AboutRepositoryTableViewController: BasicStaticTableViewController {
// clear current contents (if any)
self.tableData.removeAll(keepingCapacity: true)
self.tableView.reloadData()
indicatorLabel.isHidden = false
indicator.startAnimating()
// reload the table
DispatchQueue.global(qos: .userInitiated).async {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = NumberFormatter.Style.decimal
let fm = FileManager.default
let passwordEntities = self.passwordStore.fetchPasswordEntityCoreData(withDir: false)
let numberOfPasswords = numberFormatter.string(from: NSNumber(value: passwordEntities.count))!
var size = UInt64(0)
do {
if fm.fileExists(atPath: self.passwordStore.storeURL.path) {
size = try fm.allocatedSizeOfDirectoryAtURL(directoryURL: self.passwordStore.storeURL)
}
} catch {
print(error)
}
let sizeOfRepository = ByteCountFormatter.string(fromByteCount: Int64(size), countStyle: ByteCountFormatter.CountStyle.file)
let numberOfPasswordsString = numberFormatter.string(from: NSNumber(value: self.passwordStore.numberOfPasswords))!
let sizeOfRepositoryString = ByteCountFormatter.string(fromByteCount: Int64(self.passwordStore.sizeOfRepositoryByteCount), countStyle: ByteCountFormatter.CountStyle.file)
let numberOfCommits = self.passwordStore.storeRepository?.numberOfCommits(inCurrentBranch: NSErrorPointer(nilLiteral: ())) ?? 0
let numberOfCommitsString = numberFormatter.string(from: NSNumber(value: numberOfCommits))!
DispatchQueue.main.async { [weak self] in
let type = UITableViewCellAccessoryType.none
self?.tableData = [
// section 0
[[.style: CellDataStyle.value1, .accessoryType: type, .title: "Passwords", .detailText: numberOfPasswords],
[.style: CellDataStyle.value1, .accessoryType: type, .title: "Size", .detailText: sizeOfRepository], [.style: CellDataStyle.value1, .accessoryType: type, .title: "Unsynced", .detailText: String(self?.passwordStore.getNumberOfUnsyncedPasswords() ?? 0)],
[[.style: CellDataStyle.value1, .accessoryType: type, .title: "Passwords", .detailText: numberOfPasswordsString],
[.style: CellDataStyle.value1, .accessoryType: type, .title: "Size", .detailText: sizeOfRepositoryString],
[.style: CellDataStyle.value1, .accessoryType: type, .title: "Local Commits", .detailText: String(self?.passwordStore.numberOfLocalCommits() ?? 0)],
[.style: CellDataStyle.value1, .accessoryType: type, .title: "Last Synced", .detailText: Utils.getLastUpdatedTimeString()],
[.style: CellDataStyle.value1, .accessoryType: type, .title: "Commits", .detailText: numberOfCommitsString],
[.title: "Commit Logs", .action: "segue", .link: "showCommitLogsSegue"],
],
]
self?.indicator.stopAnimating()
self?.indicatorLabel.isHidden = true
self?.tableView.reloadData()
}
}

View file

@ -14,6 +14,7 @@ class AboutTableViewController: BasicStaticTableViewController {
tableData = [
// section 0
[[.title: "Website", .action: "link", .link: "https://github.com/mssun/pass-ios.git"],
[.title: "Help", .action: "link", .link: "https://github.com/mssun/passforios/wiki"],
[.title: "Contact Developer", .action: "link", .link: "mailto:bob@mssun.me?subject=passforiOS"],],
// section 1,

View file

@ -10,8 +10,6 @@ import UIKit
import SwiftyUserDefaults
class AddPasswordTableViewController: PasswordEditorTableViewController {
var password: Password?
var tempContent: String = ""
let passwordStore = PasswordStore.shared
@ -28,20 +26,29 @@ class AddPasswordTableViewController: PasswordEditorTableViewController {
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
if identifier == "saveAddPasswordSegue" {
// check PGP key
if passwordStore.privateKey == nil {
guard passwordStore.privateKey != nil else {
let alertTitle = "Cannot Add Password"
let alertMessage = "PGP Key is not set. Please set your PGP Key first."
Utils.alert(title: alertTitle, message: alertMessage, controller: self, completion: nil)
return false
}
// check name
let nameCell = tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as! TextFieldTableViewCell
if nameCell.getContent()!.isEmpty {
guard nameCell.getContent()!.isEmpty == false else {
let alertTitle = "Cannot Add Password"
let alertMessage = "Please fill in the name."
Utils.alert(title: alertTitle, message: alertMessage, controller: self, completion: nil)
return false
}
// check "/"
guard nameCell.getContent()!.contains("/") == false else {
let alertTitle = "Cannot Add Password"
let alertMessage = "Illegal character."
Utils.alert(title: alertTitle, message: alertMessage, controller: self, completion: nil)
return false
}
}
return true
}

View file

@ -8,20 +8,34 @@
import UIKit
import SVProgressHUD
import SwiftyUserDefaults
class AdvancedSettingsTableViewController: UITableViewController {
@IBOutlet weak var encryptInASCIIArmoredTableViewCell: UITableViewCell!
@IBOutlet weak var eraseDataTableViewCell: UITableViewCell!
@IBOutlet weak var discardChangesTableViewCell: UITableViewCell!
let passwordStore = PasswordStore.shared
let encryptInASCIIArmoredSwitch: UISwitch = {
let uiSwitch = UISwitch()
uiSwitch.onTintColor = Globals.blue
uiSwitch.sizeToFit()
uiSwitch.addTarget(self, action: #selector(encryptInASCIIArmoredAction(_:)), for: UIControlEvents.valueChanged)
return uiSwitch
}()
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Advanced"
encryptInASCIIArmoredSwitch.isOn = Defaults[.encryptInArmored]
encryptInASCIIArmoredTableViewCell.accessoryView = encryptInASCIIArmoredSwitch
encryptInASCIIArmoredTableViewCell.selectionStyle = .none
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if tableView.cellForRow(at: indexPath) == eraseDataTableViewCell {
print("erase data")
let alert = UIAlertController(title: "Erase Password Store Data?", message: "This will delete all local data and settings. Password store data on your remote server will not be affected.", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Erase Password Data", style: UIAlertActionStyle.destructive, handler: {[unowned self] (action) -> Void in
SVProgressHUD.show(withStatus: "Erasing ...")
@ -32,7 +46,6 @@ class AdvancedSettingsTableViewController: UITableViewController {
}))
alert.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.cancel, handler:nil))
self.present(alert, animated: true, completion: nil)
tableView.deselectRow(at: indexPath, animated: true)
} else if tableView.cellForRow(at: indexPath) == discardChangesTableViewCell {
let alert = UIAlertController(title: "Discard All Changes?", message: "Do you want to permanently discard all changes to the local copy of your password data? You cannot undo this action.", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Discard All Changes", style: UIAlertActionStyle.destructive, handler: {[unowned self] (action) -> Void in
@ -52,9 +65,7 @@ class AdvancedSettingsTableViewController: UITableViewController {
}
SVProgressHUD.dismiss(withDelay: 1)
} catch {
DispatchQueue.main.async {
Utils.alert(title: "Error", message: error.localizedDescription, controller: self, completion: nil)
}
Utils.alert(title: "Error", message: error.localizedDescription, controller: self, completion: nil)
}
}
}
@ -62,8 +73,11 @@ class AdvancedSettingsTableViewController: UITableViewController {
}))
alert.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.cancel, handler:nil))
self.present(alert, animated: true, completion: nil)
tableView.deselectRow(at: indexPath, animated: true)
}
}
func encryptInASCIIArmoredAction(_ sender: Any?) {
Defaults[.encryptInArmored] = encryptInASCIIArmoredSwitch.isOn
}
}

View file

@ -9,15 +9,13 @@
import UIKit
class EditPasswordTableViewController: PasswordEditorTableViewController {
var password: Password?
override func viewDidLoad() {
tableData = [
[[.type: PasswordEditorCellType.textFieldCell, .title: "name", .content: password!.name]],
[[.type: PasswordEditorCellType.fillPasswordCell, .title: "password", .content: password!.password],
[.type: PasswordEditorCellType.passwordLengthCell, .title: "passwordlength"]],
[[.type: PasswordEditorCellType.textViewCell, .title: "additions", .content: password!.getAdditionsPlainText()]],
[[.type: PasswordEditorCellType.deletePasswordCell]],
]
super.viewDidLoad()
}
@ -43,10 +41,11 @@ class EditPasswordTableViewController: PasswordEditorTableViewController {
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() {
cellContents[cellTitle] = cellContent
if let contentCell = cell as? ContentTableViewCell {
let cellTitle = tableData[indexPath.section][indexPath.row][.title] as! String
if let cellContent = contentCell.getContent() {
cellContents[cellTitle] = cellContent
}
}
}
var plainText = ""

View file

@ -169,10 +169,12 @@ class GeneralSettingsTableViewController: BasicStaticTableViewController {
func hideUnknownSwitchAction(_ sender: Any?) {
Defaults[.isHideUnknownOn] = hideUnknownSwitch.isOn
NotificationCenter.default.post(name: .passwordDetailDisplaySettingChanged, object: nil)
}
func hideOTPSwitchAction(_ sender: Any?) {
Defaults[.isHideOTPOn] = hideOTPSwitch.isOn
NotificationCenter.default.post(name: .passwordDetailDisplaySettingChanged, object: nil)
}
func rememberPassphraseSwitchAction(_ sender: Any?) {
@ -184,7 +186,7 @@ class GeneralSettingsTableViewController: BasicStaticTableViewController {
func showFolderSwitchAction(_ sender: Any?) {
Defaults[.isShowFolderOn] = showFolderSwitch.isOn
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
NotificationCenter.default.post(name: .passwordDisplaySettingChanged, object: nil)
}
}

View file

@ -77,15 +77,12 @@ class GitServerSettingTableViewController: UITableViewController {
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
if identifier == "saveGitServerSettingSegue" {
if gitRepositoryURLTextField.text == "" || authenticationMethod == nil {
var alertMessage = ""
if gitRepositoryURLTextField.text == "" {
alertMessage = "Git Server is not set. Please set the Git server first."
}
if authenticationMethod == nil {
alertMessage = "Authentication method is not set. Please set your authentication method first."
}
Utils.alert(title: "Cannot Save Settings", message: alertMessage, controller: self, completion: nil)
guard let _ = URL(string: gitRepositoryURLTextField.text!) else {
Utils.alert(title: "Cannot Save", message: "Git Server is not set.", controller: self, completion: nil)
return false
}
guard authenticationMethod != nil else {
Utils.alert(title: "Cannot Save", message: "Authentication method is not set.", controller: self, completion: nil)
return false
}
}

View file

@ -26,24 +26,15 @@ class PGPKeySettingTableViewController: UITableViewController {
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
if identifier == "savePGPKeySegue" {
guard pgpPublicKeyURLTextField.text != nil else {
return false
}
guard pgpPrivateKeyURLTextField.text != nil else {
return false
}
guard URL(string: pgpPublicKeyURLTextField.text!) != nil else {
guard let pgpPublicKeyURL = URL(string: pgpPublicKeyURLTextField.text!) else {
Utils.alert(title: "Cannot Save", message: "Please set Public Key URL first.", controller: self, completion: nil)
return false
}
guard URL(string: pgpPrivateKeyURLTextField.text!) != nil else {
guard let pgpPrivateKeyURL = URL(string: pgpPrivateKeyURLTextField.text!) else {
Utils.alert(title: "Cannot Save", message: "Please set Private Key URL first.", controller: self, completion: nil)
return false
}
if URL(string: pgpPublicKeyURLTextField.text!)!.scheme! == "http" &&
URL(string: pgpPrivateKeyURLTextField.text!)!.scheme! == "http" {
guard pgpPublicKeyURL.scheme! == "https", pgpPrivateKeyURL.scheme! == "https" else {
Utils.alert(title: "Cannot Save Settings", message: "HTTP connection is not supported.", controller: self, completion: nil)
return false
}
@ -55,7 +46,9 @@ 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
self.performSegue(withIdentifier: "savePGPKeySegue", sender: self)
if self.shouldPerformSegue(withIdentifier: "savePGPKeySegue", sender: self) {
self.performSegue(withIdentifier: "savePGPKeySegue", sender: self)
}
}))
alert.addTextField(configurationHandler: {(textField: UITextField!) in
textField.text = self.pgpPassphrase

View file

@ -13,26 +13,25 @@ import SVProgressHUD
class PasswordDetailTableViewController: UITableViewController, UIGestureRecognizerDelegate {
var passwordEntity: PasswordEntity?
var passwordCategoryText = ""
var password: Password?
var passwordImage: UIImage?
var oneTimePasswordIndexPath : IndexPath?
var shouldPopCurrentView = false
let passwordStore = PasswordStore.shared
private var password: Password?
private var passwordCategoryText = ""
private var passwordImage: UIImage?
private var oneTimePasswordIndexPath : IndexPath?
private var shouldPopCurrentView = false
private let passwordStore = PasswordStore.shared
let indicator: UIActivityIndicatorView = {
private let indicator: UIActivityIndicatorView = {
let indicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
indicator.center = CGPoint(x: UIScreen.main.bounds.width / 2, y: UIScreen.main.bounds.height * 0.382)
return indicator
}()
lazy var editUIBarButtonItem: UIBarButtonItem = {
private lazy var editUIBarButtonItem: UIBarButtonItem = {
let uiBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(pressEdit(_:)))
return uiBarButtonItem
}()
struct TableCell {
private struct TableCell {
var title: String
var content: String
init() {
@ -46,12 +45,12 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
}
}
struct TableSection {
private struct TableSection {
var title: String
var item: Array<TableCell>
}
var tableData = Array<TableSection>()
private var tableData = Array<TableSection>()
override func viewDidLoad() {
super.viewDidLoad()
@ -67,7 +66,6 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
tableView.contentInset = UIEdgeInsetsMake(-36, 0, 0, 0);
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 52
indicator.startAnimating()
tableView.addSubview(indicator)
@ -96,16 +94,35 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
self.present(alert, animated: true, completion: nil)
}
self.setupUpdateOneTimePassword()
self.addNotificationObservers()
self.setupOneTimePasswordAutoRefresh()
// pop the current view because this password might be "discarded"
NotificationCenter.default.addObserver(self, selector: #selector(setShouldPopCurrentView), name: .passwordStoreChangeDiscarded, object: nil)
// reset the data table if some password (maybe another one) has been updated
NotificationCenter.default.addObserver(self, selector: #selector(showPassword), name: .passwordStoreUpdated, object: nil)
// reset the data table if the disaply settings have been changed
NotificationCenter.default.addObserver(self, selector: #selector(showPassword), name: .passwordDetailDisplaySettingChanged, object: nil)
}
func decryptThenShowPassword(passphrase: String) {
override func viewDidAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if self.shouldPopCurrentView {
let alert = UIAlertController(title: "Notice", message: "All previous local changes have been discarded. Your current Password Store will be shown.", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: {_ in
_ = self.navigationController?.popViewController(animated: true)
}))
self.present(alert, animated: true, completion: nil)
}
}
private func decryptThenShowPassword(passphrase: String) {
if Defaults[.isRememberPassphraseOn] {
self.passwordStore.pgpKeyPassphrase = passphrase
}
DispatchQueue.global(qos: .userInitiated).async {
// decrypt password
do {
self.password = try self.passwordEntity!.decrypt(passphrase: passphrase)!
} catch {
@ -118,39 +135,42 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
}
return
}
let password = self.password!
DispatchQueue.main.async { [weak self] in
self?.showPassword(password: password)
// display password
self.showPassword()
}
}
@objc private func showPassword() {
DispatchQueue.main.async { [weak self] in
self?.indicator.stopAnimating()
self?.setTableData()
UIView.performWithoutAnimation {
self?.tableView.reloadData()
// add layoutIfNeeded solves the "flickering problem" during refresh
self?.tableView.layoutIfNeeded()
}
self?.editUIBarButtonItem.isEnabled = true
if let urlString = self?.password?.getURLString() {
if self?.passwordEntity?.image == nil {
self?.updatePasswordImage(urlString: urlString)
}
}
}
}
func showPassword(password: Password) {
setTableData()
self.tableView.reloadData()
indicator.stopAnimating()
editUIBarButtonItem.isEnabled = true
if let urlString = password.getURLString() {
if self.passwordEntity?.image == nil{
self.updatePasswordImage(urlString: urlString)
}
}
}
func setupUpdateOneTimePassword() {
private func setupOneTimePasswordAutoRefresh() {
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
[weak self] timer in
// bail out of the timer code if the object has been freed
guard let strongSelf = self,
let token = strongSelf.password?.otpToken,
let otpType = strongSelf.password?.otpType,
otpType != .none,
let indexPath = strongSelf.oneTimePasswordIndexPath,
let cell = strongSelf.tableView.cellForRow(at: indexPath) as? LabelTableViewCell else {
return
}
switch token.generator.factor {
case .timer:
// totp
switch otpType {
case .totp:
if let (title, otp) = strongSelf.password?.getOtpStrings() {
strongSelf.tableData[indexPath.section].item[indexPath.row].title = title
strongSelf.tableData[indexPath.section].item[indexPath.row].content = otp
@ -163,16 +183,19 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
}
}
func pressEdit(_ sender: Any?) {
print("pressEdit")
@objc private func pressEdit(_ sender: Any?) {
performSegue(withIdentifier: "editPasswordSegue", sender: self)
}
@IBAction func cancelEditPassword(segue: UIStoryboardSegue) {
@objc private func setShouldPopCurrentView() {
self.shouldPopCurrentView = true
}
@IBAction private func cancelEditPassword(segue: UIStoryboardSegue) {
}
@IBAction func saveEditPassword(segue: UIStoryboardSegue) {
@IBAction private func saveEditPassword(segue: UIStoryboardSegue) {
if self.password!.changed {
SVProgressHUD.show(withStatus: "Saving")
DispatchQueue.global(qos: .userInitiated).async {
@ -193,7 +216,13 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
}
}
func setTableData() {
@IBAction private func deletePassword(segue: UIStoryboardSegue) {
print("delete")
passwordStore.delete(passwordEntity: passwordEntity!)
let _ = navigationController?.popViewController(animated: true)
}
private func setTableData() {
self.tableData = Array<TableSection>()
tableData.append(TableSection(title: "", item: []))
tableData[0].item.append(TableCell())
@ -206,26 +235,12 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
self.tableData[tableDataIndex].item.append(TableCell(title: "password", content: password.password))
// show one time password
if let token = password.otpToken {
switch token.generator.factor {
case .counter(_):
// counter-based one time password
if password.otpType != .none {
if let (title, otp) = self.password?.getOtpStrings() {
self.tableData.append(TableSection(title: "One time password", item: []))
tableDataIndex += 1
oneTimePasswordIndexPath = IndexPath(row: 0, section: tableDataIndex)
if let crtPassword = password.otpToken?.currentPassword {
self.tableData[tableDataIndex].item.append(TableCell(title: "HMAC-based", content: crtPassword))
}
case .timer(let period):
// time-based one time password
self.tableData.append(TableSection(title: "One time password", item: []))
tableDataIndex += 1
oneTimePasswordIndexPath = IndexPath(row: 0, section: tableDataIndex)
if let crtPassword = password.otpToken?.currentPassword {
let timeSinceEpoch = Date().timeIntervalSince1970
let validTime = Int(period - timeSinceEpoch.truncatingRemainder(dividingBy: period))
self.tableData[tableDataIndex].item.append(TableCell(title: "time-based (expiring in \(validTime)s)", content: crtPassword))
}
self.tableData[tableDataIndex].item.append(TableCell(title: title, content: otp))
}
}
@ -233,7 +248,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
let filteredAdditionKeys = password.additionKeys.filter {
$0.lowercased() != "username" &&
$0.lowercased() != "password" &&
(!$0.hasPrefix("unknown") || !Defaults[.isHideOTPOn]) &&
(!$0.hasPrefix("unknown") || !Defaults[.isHideUnknownOn]) &&
(!Password.otpKeywords.contains($0) || !Defaults[.isHideOTPOn]) }
if filteredAdditionKeys.count > 0 {
@ -255,7 +270,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
}
}
func updatePasswordImage(urlString: String) {
private func updatePasswordImage(urlString: String) {
var newUrlString = urlString
if urlString.lowercased().hasPrefix("http://") {
// try to replace http url to https url
@ -294,7 +309,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
}
}
func tapMenu(recognizer: UITapGestureRecognizer) {
@objc private func tapMenu(recognizer: UITapGestureRecognizer) {
if recognizer.state == UIGestureRecognizerState.ended {
let tapLocation = recognizer.location(in: self.tableView)
if let tapIndexPath = self.tableView.indexPathForRow(at: tapLocation) {
@ -303,9 +318,9 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
let menuController = UIMenuController.shared
let revealItem = UIMenuItem(title: "Reveal", action: #selector(LabelTableViewCell.revealPassword(_:)))
let concealItem = UIMenuItem(title: "Conceal", action: #selector(LabelTableViewCell.concealPassword(_:)))
let nextPasswordItem = UIMenuItem(title: "Next Password", action: #selector(LabelTableViewCell.nextPassword(_:)))
let nextHOTPItem = UIMenuItem(title: "Next Password", action: #selector(LabelTableViewCell.getNextHOTP(_:)))
let openURLItem = UIMenuItem(title: "Copy Password & Open Link", action: #selector(LabelTableViewCell.openLink(_:)))
menuController.menuItems = [revealItem, concealItem, nextPasswordItem, openURLItem]
menuController.menuItems = [revealItem, concealItem, nextHOTPItem, openURLItem]
menuController.setTargetRect(tappedCell.contentLabel.frame, in: tappedCell.contentLabel.superview!)
menuController.setMenuVisible(true, animated: true)
}
@ -313,6 +328,46 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
}
}
func getNextHOTP() {
guard password != nil, passwordEntity != nil, password?.otpType == .hotp else {
DispatchQueue.main.async {
Utils.alert(title: "Error", message: "Get next password of a non-HOTP entry.", controller: self, completion: nil)
}
return;
}
// increase HOTP counter
password!.increaseHotpCounter()
// copy HOTP to pasteboard
if let plainPassword = password!.getOtp() {
Utils.copyToPasteboard(textToCopy: plainPassword)
}
// commit the change of HOTP counter
if password!.changed {
DispatchQueue.global(qos: .userInitiated).async {
self.passwordStore.update(passwordEntity: self.passwordEntity!, password: self.password!, progressBlock: {_ in })
DispatchQueue.main.async {
self.passwordEntity!.synced = false
self.passwordStore.saveUpdated(passwordEntity: self.passwordEntity!)
SVProgressHUD.showSuccess(withStatus: "Password Copied\nCounter Updated")
SVProgressHUD.dismiss(withDelay: 1)
}
}
}
}
func openLink() {
guard let urlString = self.password?.getURLString(), let url = URL(string: urlString) else {
DispatchQueue.main.async {
Utils.alert(title: "Error", message: "Cannot find a valid URL", controller: self, completion: nil)
}
return;
}
Utils.copyToPasteboard(textToCopy: password?.password)
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
override func numberOfSections(in tableView: UITableView) -> Int {
return tableData.count
@ -340,7 +395,7 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
let cell = tableView.dequeueReusableCell(withIdentifier: "labelCell", for: indexPath) as! LabelTableViewCell
let titleData = tableData[sectionIndex].item[rowIndex].title
let contentData = tableData[sectionIndex].item[rowIndex].content
cell.passwordTableView = self
cell.delegatePasswordTableView = self
cell.isPasswordCell = (titleData.lowercased() == "password" ? true : false)
cell.isURLCell = (titleData.lowercased() == "url" ? true : false)
cell.isHOTPCell = (titleData == "HMAC-based" ? true : false)
@ -381,23 +436,4 @@ class PasswordDetailTableViewController: UITableViewController, UIGestureRecogni
override func tableView(_ tableView: UITableView, shouldShowMenuForRowAt indexPath: IndexPath) -> Bool {
return true
}
private func addNotificationObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(setShouldPopCurrentView), name: .passwordStoreChangeDiscarded, object: nil)
}
func setShouldPopCurrentView() {
self.shouldPopCurrentView = true
}
override func viewDidAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if self.shouldPopCurrentView {
let alert = UIAlertController(title: "Notice", message: "All previous local changes have been discarded. Your current Password Store will be shown.", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: {_ in
_ = self.navigationController?.popViewController(animated: true)
}))
self.present(alert, animated: true, completion: nil)
}
}
}

View file

@ -8,23 +8,40 @@
import UIKit
enum PasswordEditorCellType {
case textFieldCell, textViewCell, fillPasswordCell, passwordLengthCell
case textFieldCell, textViewCell, fillPasswordCell, passwordLengthCell, deletePasswordCell
}
enum PasswordEditorCellKey {
case type, title, content, placeholders
}
class PasswordEditorTableViewController: UITableViewController, FillPasswordTableViewCellDelegate {
var navigationItemTitle: String?
class PasswordEditorTableViewController: UITableViewController, FillPasswordTableViewCellDelegate, PasswordSettingSliderTableViewCellDelegate, UIGestureRecognizerDelegate {
var tableData = [
[Dictionary<PasswordEditorCellKey, Any>]
]()
var sectionHeaderTitles = ["name", "password", "additions"].map {$0.uppercased()}
var sectionFooterTitles = ["", "", "It is recommended to use \"key: value\" format to store additional fields as follows:\n url: https://www.apple.com\n username: passforios@gmail.com."]
]()
var password: Password?
private var navigationItemTitle: String?
private var sectionHeaderTitles = ["name", "password", "additions",""].map {$0.uppercased()}
private var sectionFooterTitles = ["", "", "Use \"key: value\" format for additional fields.", ""]
private let passwordSection = 1
private var hidePasswordSettings = true
private var fillPasswordCell: FillPasswordTableViewCell?
private var passwordLengthCell: SliderTableViewCell?
private var deletePasswordCell: UITableViewCell?
override func loadView() {
super.loadView()
deletePasswordCell = UITableViewCell(style: .default, reuseIdentifier: "default")
deletePasswordCell!.textLabel?.text = "Delete Password"
deletePasswordCell!.textLabel?.textColor = Globals.red
deletePasswordCell?.selectionStyle = .default
}
var passwordLengthCell: SliderTableViewCell?
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = navigationItemTitle
@ -35,33 +52,40 @@ class PasswordEditorTableViewController: UITableViewController, FillPasswordTabl
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 48
tableView.allowsSelection = false
self.tableView.sectionFooterHeight = UITableViewAutomaticDimension;
self.tableView.estimatedSectionFooterHeight = 0;
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tableTapped))
tapGesture.delegate = self
tapGesture.cancelsTouchesInView = false
tableView.addGestureRecognizer(tapGesture)
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellData = tableData[indexPath.section][indexPath.row]
var cell = ContentTableViewCell()
switch cellData[PasswordEditorCellKey.type] as! PasswordEditorCellType {
case .textViewCell:
cell = tableView.dequeueReusableCell(withIdentifier: "textViewCell", for: indexPath) as! ContentTableViewCell
let cell = tableView.dequeueReusableCell(withIdentifier: "textViewCell", for: indexPath) as! ContentTableViewCell
cell.setContent(content: cellData[PasswordEditorCellKey.content] as? String)
return cell
case .fillPasswordCell:
let fillPasswordCell = tableView.dequeueReusableCell(withIdentifier: "fillPasswordCell", for: indexPath) as?FillPasswordTableViewCell
fillPasswordCell = tableView.dequeueReusableCell(withIdentifier: "fillPasswordCell", for: indexPath) as? FillPasswordTableViewCell
fillPasswordCell?.delegate = self
cell = fillPasswordCell!
fillPasswordCell?.setContent(content: cellData[PasswordEditorCellKey.content] as? String)
return fillPasswordCell!
case .passwordLengthCell:
passwordLengthCell = tableView.dequeueReusableCell(withIdentifier: "passwordLengthCell", for: indexPath) as? SliderTableViewCell
passwordLengthCell?.reset(title: "Length", minimumValue: Globals.passwordMinimumLength, maximumValue: Globals.passwordMaximumLength, defaultValue: Globals.passwordDefaultLength)
cell = passwordLengthCell!
passwordLengthCell?.delegate = self
return passwordLengthCell!
case .deletePasswordCell:
return deletePasswordCell!
default:
cell = tableView.dequeueReusableCell(withIdentifier: "textFieldCell", for: indexPath) as! ContentTableViewCell
let cell = tableView.dequeueReusableCell(withIdentifier: "textFieldCell", for: indexPath) as! ContentTableViewCell
cell.setContent(content: cellData[PasswordEditorCellKey.content] as? String)
return cell
}
if let content = cellData[PasswordEditorCellKey.content] as? String {
cell.setContent(content: content)
}
return cell
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
@ -73,9 +97,14 @@ class PasswordEditorTableViewController: UITableViewController, FillPasswordTabl
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableData[section].count
if section == passwordSection, hidePasswordSettings {
// hide the password section, only the password should be shown
return 1
} else {
return tableData[section].count
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return sectionHeaderTitles[section]
}
@ -84,8 +113,81 @@ class PasswordEditorTableViewController: UITableViewController, FillPasswordTabl
return sectionFooterTitles[section]
}
func generatePassword() -> String {
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if tableView.cellForRow(at: indexPath) == 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)
}
tableView.deselectRow(at: indexPath, animated: true)
}
// generate password, copy to pasteboard, and set the cell
// check whether the current password looks like an OTP field
func generateAndCopyPassword() {
if let currentPassword = fillPasswordCell?.getContent(),
Password.LooksLikeOTP(line: currentPassword) {
let alert = UIAlertController(title: "Overwrite?", message: "Overwrite the one-time password configuration?", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Yes", style: UIAlertActionStyle.destructive, handler: {_ in
self.generateAndCopyPasswordNoOtpCheck()
}))
alert.addAction(UIAlertAction(title: "Cancel", style: UIAlertActionStyle.cancel, handler: nil))
self.present(alert, animated: true, completion: nil)
} else {
self.generateAndCopyPasswordNoOtpCheck()
}
}
// generate the password, don't care whether the original line is otp
func generateAndCopyPasswordNoOtpCheck() {
// show password settings (e.g., the length slider)
if hidePasswordSettings == true {
hidePasswordSettings = false
tableView.reloadSections([passwordSection], with: .fade)
}
let length = passwordLengthCell?.roundedValue ?? Globals.passwordDefaultLength
return Utils.generatePassword(length: length)
let plainPassword = Utils.generatePassword(length: length)
Utils.copyToPasteboard(textToCopy: plainPassword)
// update tableData so to make sure reloadData() works correctly
tableData[passwordSection][0][PasswordEditorCellKey.content] = plainPassword
// update cell manually, no need to call reloadData()
fillPasswordCell?.setContent(content: plainPassword)
}
func tableTapped(recognizer: UITapGestureRecognizer) {
if recognizer.state == UIGestureRecognizerState.ended {
let tapLocation = recognizer.location(in: self.tableView)
let tapIndexPath = self.tableView.indexPathForRow(at: tapLocation)
// do nothing, if delete is tapped (a temporary solution)
if tapIndexPath != nil, deletePasswordCell != nil,
tableView.cellForRow(at: tapIndexPath!) == deletePasswordCell {
return
}
// hide password settings (e.g., the length slider)
if tapIndexPath?.section != passwordSection, hidePasswordSettings == false {
hidePasswordSettings = true
tableView.reloadSections([passwordSection], with: .fade)
// select the row at tapIndexPath manually
if tapIndexPath != nil {
self.tableView(self.tableView, didSelectRowAt: tapIndexPath!)
}
}
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is UITapGestureRecognizer {
// so that the tap gesture could be passed by
return true
} else {
return false
}
}
}

View file

@ -25,13 +25,14 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
private var passwordsTableEntries: [PasswordsTableEntry] = []
private var filteredPasswordsTableEntries: [PasswordsTableEntry] = []
private var parentPasswordEntity: PasswordEntity? = nil
let passwordStore = PasswordStore.shared
private let passwordStore = PasswordStore.shared
private var tapTabBarTime: TimeInterval = 0
var sections : [(index: Int, length :Int, title: String)] = Array()
var searchActive : Bool = false
lazy var searchController: UISearchController = {
private var sections : [(index: Int, length :Int, title: String)] = Array()
private var searchActive : Bool = false
private lazy var searchController: UISearchController = {
let uiSearchController = UISearchController(searchResultsController: nil)
uiSearchController.searchResultsUpdater = self
uiSearchController.dimsBackgroundDuringPresentation = false
@ -40,20 +41,40 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
uiSearchController.searchBar.sizeToFit()
return uiSearchController
}()
lazy var refreshControl: UIRefreshControl = {
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(PasswordsViewController.handleRefresh(_:)), for: UIControlEvents.valueChanged)
return refreshControl
private lazy var syncControl: UIRefreshControl = {
let syncControl = UIRefreshControl()
syncControl.addTarget(self, action: #selector(handleRefresh(_:)), for: UIControlEvents.valueChanged)
return syncControl
}()
lazy var searchBarView: UIView = {
let uiView = UIView(frame: CGRect(x: 0, y: 64, width: UIScreen.main.bounds.width, height: 44))
private lazy var searchBarView: UIView = {
let uiView = UIView(frame: CGRect(x: 0, y: 64, width: self.view.bounds.width, height: 44))
uiView.addSubview(self.searchController.searchBar)
return uiView
}()
lazy var backUIBarButtonItem: UIBarButtonItem = {
private lazy var backUIBarButtonItem: UIBarButtonItem = {
let backUIBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(self.backAction(_:)))
return backUIBarButtonItem
}()
private lazy var transitionFromRight: CATransition = {
let transition = CATransition()
transition.type = kCATransitionPush
transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
transition.fillMode = kCAFillModeForwards
transition.duration = 0.25
transition.subtype = kCATransitionFromRight
return transition
}()
private lazy var transitionFromLeft: CATransition = {
let transition = CATransition()
transition.type = kCATransitionPush
transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
transition.fillMode = kCAFillModeForwards
transition.duration = 0.25
transition.subtype = kCATransitionFromLeft
return transition
}()
@IBOutlet weak var tableView: UITableView!
@ -102,11 +123,11 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
}
}
func syncPasswords() {
private func syncPasswords() {
SVProgressHUD.setDefaultMaskType(.black)
SVProgressHUD.setDefaultStyle(.light)
SVProgressHUD.show(withStatus: "Sync Password Store")
let numberOfUnsyncedPasswords = self.passwordStore.getNumberOfUnsyncedPasswords()
let numberOfLocalCommits = self.passwordStore.numberOfLocalCommits()
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
do {
try self.passwordStore.pullRepository(transferProgressBlock: {(git_transfer_progress, stop) in
@ -114,7 +135,7 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
SVProgressHUD.showProgress(Float(git_transfer_progress.pointee.received_objects)/Float(git_transfer_progress.pointee.total_objects), status: "Pull Remote Repository")
}
})
if numberOfUnsyncedPasswords > 0 {
if numberOfLocalCommits > 0 {
try self.passwordStore.pushRepository(transferProgressBlock: {(current, total, bytes, stop) in
DispatchQueue.main.async {
SVProgressHUD.showProgress(Float(current)/Float(total), status: "Push Remote Repository")
@ -122,11 +143,7 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
})
}
DispatchQueue.main.async {
self.passwordStore.updatePasswordEntityCoreData()
self.initPasswordsTableEntries(parent: nil)
self.reloadTableView(data: self.passwordsTableEntries)
self.passwordStore.setAllSynced()
self.setNavigationItemTitle()
self.reloadTableView(parent: nil)
Defaults[.lastUpdatedTime] = Date()
Defaults[.gitRepositoryPasswordAttempts] = 0
SVProgressHUD.showSuccess(withStatus: "Done")
@ -140,26 +157,27 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
}
}
private func addNotificationObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(PasswordsViewController.actOnPasswordStoreUpdatedNotification), name: .passwordStoreUpdated, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(PasswordsViewController.actOnSearchNotification), name: .passwordSearch, object: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
setNavigationItemTitle()
initPasswordsTableEntries(parent: nil)
addNotificationObservers()
generateSections(item: passwordsTableEntries)
tabBarController!.delegate = self
tableView.delegate = self
tableView.dataSource = self
definesPresentationContext = true
view.addSubview(searchBarView)
tableView.insertSubview(refreshControl, at: 0)
tableView.insertSubview(syncControl, at: 0)
SVProgressHUD.setDefaultMaskType(.black)
updateRefreshControlTitle()
tableView.register(UINib(nibName: "PasswordWithFolderTableViewCell", bundle: nil), forCellReuseIdentifier: "passwordWithFolderTableViewCell")
// initialize the password table
reloadTableView(parent: nil)
// reset the data table if some password (maybe another one) has been updated
NotificationCenter.default.addObserver(self, selector: #selector(reloadTableView as (Void) -> Void), name: .passwordStoreUpdated, object: nil)
// reset the data table if the disaply settings have been changed
NotificationCenter.default.addObserver(self, selector: #selector(reloadTableView as (Void) -> Void), name: .passwordDisplaySettingChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(actOnSearchNotification), name: .passwordSearch, object: nil)
}
override func viewWillAppear(_ animated: Bool) {
@ -243,15 +261,13 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
} else {
tableView.deselectRow(at: indexPath, animated: true)
searchController.isActive = false
initPasswordsTableEntries(parent: entry.passwordEntity)
reloadTableView(data: passwordsTableEntries)
reloadTableView(parent: entry.passwordEntity, anim: transitionFromRight)
}
}
func backAction(_ sender: Any?) {
guard Defaults[.isShowFolderOn] else { return }
initPasswordsTableEntries(parent: parentPasswordEntity?.parent)
reloadTableView(data: passwordsTableEntries)
reloadTableView(parent: parentPasswordEntity?.parent, anim: transitionFromLeft)
}
func longPressAction(_ gesture: UILongPressGestureRecognizer) {
@ -279,7 +295,7 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
copyToPasteboard(from: indexPath)
}
func copyToPasteboard(from indexPath: IndexPath) {
private func copyToPasteboard(from indexPath: IndexPath) {
guard self.passwordStore.privateKey != nil else {
Utils.alert(title: "Cannot Copy Password", message: "PGP Key is not set. Please set your PGP Key first.", controller: self, completion: nil)
return
@ -305,7 +321,7 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
}
func decryptThenCopyPassword(passwordEntity: PasswordEntity, passphrase: String) {
private func decryptThenCopyPassword(passwordEntity: PasswordEntity, passphrase: String) {
SVProgressHUD.setDefaultMaskType(.black)
SVProgressHUD.setDefaultStyle(.dark)
SVProgressHUD.show(withStatus: "Decrypting")
@ -327,7 +343,7 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
}
}
func generateSections(item: [PasswordsTableEntry]) {
private func generateSections(item: [PasswordsTableEntry]) {
sections.removeAll()
guard item.count != 0 else {
return
@ -349,27 +365,6 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
sections.append(newSection)
}
func actOnPasswordStoreUpdatedNotification() {
initPasswordsTableEntries(parent: nil)
reloadTableView(data: passwordsTableEntries)
setNavigationItemTitle()
}
private func setNavigationItemTitle() {
var title = ""
if parentPasswordEntity != nil {
title = parentPasswordEntity!.name!
} else {
title = "Password Store"
}
let numberOfUnsynced = self.passwordStore.getNumberOfUnsyncedPasswords()
if numberOfUnsynced == 0 {
navigationItem.title = "\(title)"
} else {
navigationItem.title = "\(title) (\(numberOfUnsynced))"
}
}
func actOnSearchNotification() {
searchController.searchBar.becomeFirstResponder()
}
@ -410,27 +405,47 @@ class PasswordsViewController: UIViewController, UITableViewDataSource, UITableV
}
}
func updateRefreshControlTitle() {
var atribbutedTitle = "Pull to Sync Password Store"
atribbutedTitle = "Last Synced: \(Utils.getLastUpdatedTimeString())"
refreshControl.attributedTitle = NSAttributedString(string: atribbutedTitle)
}
func reloadTableView(data: [PasswordsTableEntry]) {
setNavigationItemTitle()
private func reloadTableView(data: [PasswordsTableEntry], anim: CAAnimation? = nil) {
// set navigation item
var numberOfLocalCommitsString = ""
let numberOfLocalCommits = self.passwordStore.numberOfLocalCommits()
if numberOfLocalCommits > 0 {
numberOfLocalCommitsString = " (\(numberOfLocalCommits))"
}
if parentPasswordEntity != nil {
navigationItem.title = "\(parentPasswordEntity!.name!)\(numberOfLocalCommitsString)"
navigationItem.leftBarButtonItem = backUIBarButtonItem
} else {
navigationItem.title = "Password Store\(numberOfLocalCommitsString)"
navigationItem.leftBarButtonItem = nil
}
// set the password table
generateSections(item: data)
if anim != nil {
self.tableView.layer.add(anim!, forKey: "UITableViewReloadDataAnimationKey")
}
tableView.reloadData()
updateRefreshControlTitle()
self.tableView.layer.removeAnimation(forKey: "UITableViewReloadDataAnimationKey")
// set the sync control title
let atribbutedTitle = "Last Synced: \(Utils.getLastUpdatedTimeString())"
syncControl.attributedTitle = NSAttributedString(string: atribbutedTitle)
}
func handleRefresh(_ refreshControl: UIRefreshControl) {
private func reloadTableView(parent: PasswordEntity?, anim: CAAnimation? = nil) {
initPasswordsTableEntries(parent: parent)
reloadTableView(data: passwordsTableEntries, anim: anim)
}
func reloadTableView() {
initPasswordsTableEntries(parent: nil)
reloadTableView(data: passwordsTableEntries)
}
func handleRefresh(_ syncControl: UIRefreshControl) {
syncPasswords()
refreshControl.endRefreshing()
syncControl.endRefreshing()
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {

View file

@ -32,22 +32,22 @@ class SSHKeySettingTableViewController: UITableViewController {
}
func doneButtonTapped(_ sender: UIButton) {
guard URL(string: publicKeyURLTextField.text!) != nil else {
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
}
guard URL(string: privateKeyURLTextField.text!) != nil else {
guard let privateKeyURL = URL(string: privateKeyURLTextField.text!) else {
Utils.alert(title: "Cannot Save", message: "Please set Private Key URL first.", controller: self, completion: nil)
return
}
Defaults[.gitRepositorySSHPublicKeyURL] = URL(string: publicKeyURLTextField.text!)
Defaults[.gitRepositorySSHPrivateKeyURL] = URL(string: privateKeyURLTextField.text!)
Defaults[.gitRepositorySSHPublicKeyURL] = publicKeyURL
Defaults[.gitRepositorySSHPrivateKeyURL] = privateKeyURL
Utils.addPasswordToKeychain(name: "gitRepositorySSHPrivateKeyPassphrase", password: passphraseTextField.text!)
do {
try Data(contentsOf: Defaults[.gitRepositorySSHPublicKeyURL]!).write(to: Globals.sshPublicKeyURL, options: .atomic)
try Data(contentsOf: Defaults[.gitRepositorySSHPrivateKeyURL]!).write(to: Globals.sshPrivateKeyURL, options: .atomic)
try Data(contentsOf: publicKeyURL).write(to: Globals.sshPublicKeyURL, options: .atomic)
try Data(contentsOf: privateKeyURL).write(to: Globals.sshPrivateKeyURL, options: .atomic)
} catch {
Utils.alert(title: "Error", message: error.localizedDescription, controller: self, completion: nil)
}

View file

@ -140,7 +140,6 @@ class SettingsTableViewController: UITableViewController {
}
})
DispatchQueue.main.async {
self.passwordStore.updatePasswordEntityCoreData()
Defaults[.lastUpdatedTime] = Date()
Defaults[.gitRepositoryURL] = URL(string: gitRepostiroyURL)
Defaults[.gitRepositoryUsername] = username

View file

@ -10,7 +10,6 @@ import Foundation
import SwiftyUserDefaults
extension DefaultsKeys {
// static let pgpKeyURL = DefaultsKey<URL?>("pgpKeyURL")
static let pgpKeySource = DefaultsKey<String?>("pgpKeySource")
static let pgpPublicKeyURL = DefaultsKey<URL?>("pgpPublicKeyURL")
static let pgpPrivateKeyURL = DefaultsKey<URL?>("pgpPrivateKeyURL")
@ -35,6 +34,6 @@ extension DefaultsKeys {
static let isRememberPassphraseOn = DefaultsKey<Bool>("isRememberPassphraseOn")
static let isShowFolderOn = DefaultsKey<Bool>("isShowFolderOn")
static let passwordGeneratorFlavor = DefaultsKey<String>("passwordGeneratorFlavor")
static let encryptInArmored = DefaultsKey<Bool>("encryptInArmored")
}

View file

@ -28,6 +28,9 @@ class Globals {
static let passwordMaximumLength = 24
static let passwordDefaultLength = 16
static let passwordDots = "••••••••••••"
static let passwordFonts = "Menlo"
private init() { }
}

View file

@ -13,4 +13,7 @@ extension Notification.Name {
static let passwordStoreErased = Notification.Name("passwordStoreErased")
static let passwordStoreChangeDiscarded = Notification.Name("passwordStoreChangeDiscarded")
static let passwordSearch = Notification.Name("passwordSearch")
static let passwordDisplaySettingChanged = Notification.Name("passwordDisplaySettingChanged")
static let passwordDetailDisplaySettingChanged = Notification.Name("passwordDetailDisplaySettingChanged")
}

View file

@ -133,7 +133,7 @@ class Utils {
for (index, element) in plainPassword.unicodeScalars.enumerated() {
if NSCharacterSet.decimalDigits.contains(element) {
attributedPassword.addAttribute(NSForegroundColorAttributeName, value: Globals.red, range: NSRange(location: index, length: 1))
} else if NSCharacterSet.punctuationCharacters.contains(element) {
} else if !NSCharacterSet.letters.contains(element) {
attributedPassword.addAttribute(NSForegroundColorAttributeName, value: Globals.blue, range: NSRange(location: index, length: 1))
}
}

View file

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.2.1</string>
<string>0.2.2</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>ITSAppUsesNonExemptEncryption</key>

View file

@ -17,16 +17,35 @@ struct AdditionField {
}
class Password {
static let otpKeywords = ["otp_secret", "otp_type", "otp_algorithm", "otp_period", "otp_digits", "otp_counter"]
static let otpKeywords = ["otp_secret", "otp_type", "otp_algorithm", "otp_period", "otp_digits", "otp_counter", "otpauth"]
var name = ""
var password = ""
var additions = [String: String]()
var additionKeys = [String]()
var plainText = ""
var changed = false
var firstLineIsOTPField = false
var otpToken: Token?
private var plainText = ""
private var firstLineIsOTPField = false
private var otpToken: Token?
enum OtpType {
case totp, hotp, none
}
var otpType: OtpType {
get {
guard let token = self.otpToken else {
return OtpType.none
}
switch token.generator.factor {
case .counter:
return OtpType.hotp
case .timer:
return OtpType.totp
}
}
}
init(name: String, plainText: String) {
self.initEverything(name: name, plainText: plainText)
@ -39,7 +58,7 @@ class Password {
}
}
func initEverything(name: String, plainText: String) {
private func initEverything(name: String, plainText: String) {
self.name = name
self.plainText = plainText
@ -57,7 +76,7 @@ class Password {
// check whether the first line of the plainText looks like an otp entry
let (key, value) = Password.getKeyValuePair(from: plainTextSplit[0])
if key != nil && Password.otpKeywords.contains(key!) {
if Password.otpKeywords.contains(key ?? "") {
firstLineIsOTPField = true
self.additions[key!] = value
self.additionKeys.insert(key!, at: 0)
@ -79,7 +98,7 @@ class Password {
// return a key-value pair from the line
// key might be nil, if there is no ":" in the line
static func getKeyValuePair(from line: String) -> (key: String?, value: String) {
static private func getKeyValuePair(from line: String) -> (key: String?, value: String) {
let items = line.characters.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: true).map(String.init)
var key : String?
var value = ""
@ -92,7 +111,7 @@ class Password {
return (key, value)
}
static func getAdditionFields(from additionFieldsPlainText: String) -> ([String: String], [String]){
static private func getAdditionFields(from additionFieldsPlainText: String) -> ([String: String], [String]){
var additions = [String: String]()
var additionKeys = [String]()
var unknownIndex = 0
@ -125,7 +144,7 @@ class Password {
}
}
func getPlainText() -> String {
private func getPlainText() -> String {
return self.plainText
}
@ -140,21 +159,37 @@ class Password {
/*
Set otpType and otpToken, if we are able to construct a valid token.
Example of TOTP fields
Example of TOTP otpauth
(Key Uri Format: https://github.com/google/google-authenticator/wiki/Key-Uri-Format)
otpauth://totp/totp-secret?secret=AAAAAAAAAAAAAAAA&issuer=totp-secret
Example of TOTP fields [Legacy, lower priority]
otp_secret: secretsecretsecretsecretsecretsecret
otp_type: totp
otp_algorithm: sha1 (default: sha1, optional)
otp_period: 30 (default: 30, optional)
otp_digits: 6 (default: 6, optional)
Example of HOTP fields
Example of HOTP fields [Legacy, lower priority]
otp_secret: secretsecretsecretsecretsecretsecret
otp_type: hotp
otp_counter: 1
otp_digits: 6 (default: 6, optional)
*/
func updateOtpToken() {
private func updateOtpToken() {
// get otpauth, if we are able to generate a token, return
if var otpauthString = getAdditionValue(withKey: "otpauth") {
if !otpauthString.hasPrefix("otpauth:") {
otpauthString = "otpauth:\(otpauthString)"
}
if let otpauthUrl = URL(string: otpauthString),
let token = Token(url: otpauthUrl) {
self.otpToken = token
return
}
}
// get secret data
guard let secretString = getAdditionValue(withKey: "otp_secret"),
let secretData = MF_Base32Codec.data(fromBase32String: secretString),
@ -175,11 +210,11 @@ class Password {
if let algoString = getAdditionValue(withKey: "otp_algorithm") {
switch algoString.lowercased() {
case "sha256":
algorithm = Generator.Algorithm.sha256
algorithm = .sha256
case "sha512":
algorithm = Generator.Algorithm.sha512
algorithm = .sha512
default:
algorithm = Generator.Algorithm.sha1
algorithm = .sha1
}
}
@ -259,4 +294,18 @@ class Password {
let otp = self.otpToken?.currentPassword ?? "error"
return (description, otp)
}
// return the password strings
func getOtp() -> String? {
if let otp = self.otpToken?.currentPassword {
return otp
} else {
return nil
}
}
static func LooksLikeOTP(line: String) -> Bool {
let (key, _) = getKeyValuePair(from: line)
return Password.otpKeywords.contains(key ?? "")
}
}

View file

@ -35,7 +35,7 @@ extension PasswordEntity {
name = password.name
let plainData = password.getPlainData()
let pgp = PasswordStore.shared.pgp
let encryptedData = try pgp.encryptData(plainData, usingPublicKey: pgp.getKeysOf(.public)[0], armored: false)
let encryptedData = try pgp.encryptData(plainData, usingPublicKey: pgp.getKeysOf(.public)[0], armored: Defaults[.encryptInArmored])
return encryptedData
}

View file

@ -136,6 +136,23 @@ class PasswordStore {
}
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
var numberOfPasswords : Int {
return self.fetchPasswordEntityCoreData(withDir: false).count
}
var sizeOfRepositoryByteCount : UInt64 {
let fm = FileManager.default
var size = UInt64(0)
do {
if fm.fileExists(atPath: self.storeURL.path) {
size = try fm.allocatedSizeOfDirectoryAtURL(directoryURL: self.storeURL)
}
} catch {
print(error)
}
return size
}
private init() {
@ -285,6 +302,8 @@ class PasswordStore {
}
storeRepository = try GTRepository(url: storeURL)
gitCredential = credential
self.updatePasswordEntityCoreData()
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
}
@ -298,12 +317,12 @@ class PasswordStore {
]
let remote = try GTRemote(name: "origin", in: storeRepository!)
try storeRepository?.pull((storeRepository?.currentBranch())!, from: remote, withOptions: options, progress: transferProgressBlock)
self.setAllSynced()
self.updatePasswordEntityCoreData()
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
}
func updatePasswordEntityCoreData() {
private func updatePasswordEntityCoreData() {
deleteCoreData(entityName: "PasswordEntity")
let fm = FileManager.default
do {
@ -359,6 +378,9 @@ class PasswordStore {
}
func getRecentCommits(count: Int) -> [GTCommit] {
guard storeRepository != nil else {
return []
}
var commits = [GTCommit]()
do {
let enumerator = try GTEnumerator(repository: storeRepository!)
@ -482,19 +504,17 @@ class PasswordStore {
return nil
}
func createRemoveCommitInRepository(message: String, filename: String, progressBlock: (_ progress: Float) -> Void) -> GTCommit? {
func createRemoveCommitInRepository(message: String, path: String) -> GTCommit? {
do {
try storeRepository?.index().removeFile(filename)
try storeRepository?.index().removeFile(path)
try storeRepository?.index().write()
let newTree = try storeRepository!.index().writeTree()
let headReference = try storeRepository!.headReference()
let commitEnum = try GTEnumerator(repository: storeRepository!)
try commitEnum.pushSHA(headReference.targetOID.sha!)
let parent = commitEnum.nextObject() as! GTCommit
progressBlock(0.5)
let signature = gitSignatureForNow
let commit = try storeRepository!.createCommit(with: newTree, message: message, author: signature, committer: signature, parents: [parent], updatingReferenceNamed: headReference.name)
progressBlock(0.7)
return commit
} catch {
print(error)
@ -565,6 +585,18 @@ class PasswordStore {
}
}
public func delete(passwordEntity: PasswordEntity) {
Utils.removeFileIfExists(at: storeURL.appendingPathComponent(passwordEntity.path!))
let _ = createRemoveCommitInRepository(message: "Remove \(passwordEntity.nameWithCategory) from store using Pass for iOS", path: passwordEntity.path!)
context.delete(passwordEntity)
do {
try context.save()
} catch {
fatalError("Failed to delete a PasswordEntity: \(error)")
}
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
}
func saveUpdated(passwordEntity: PasswordEntity) {
do {
try context.save()
@ -634,6 +666,40 @@ class PasswordStore {
// return the number of discarded commits
func reset() throws -> Int {
// get a list of local commits
if let localCommits = try getLocalCommits(),
localCommits.count > 0 {
// get the oldest local commit
guard let firstLocalCommit = localCommits.last,
firstLocalCommit.parents.count == 1,
let newHead = firstLocalCommit.parents.first else {
throw NSError(domain: "me.mssun.pass.error", code: 1, userInfo: [NSLocalizedDescriptionKey: "Cannot decide how to reset."])
}
try self.storeRepository?.reset(to: newHead, resetType: GTRepositoryResetType.hard)
self.setAllSynced()
self.updatePasswordEntityCoreData()
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
NotificationCenter.default.post(name: .passwordStoreChangeDiscarded, object: nil)
return localCommits.count
} else {
return 0 // no new commit
}
}
func numberOfLocalCommits() -> Int {
do {
if let localCommits = try getLocalCommits() {
return localCommits.count
} else {
return 0
}
} catch {
print(error)
}
return 0
}
private func getLocalCommits() throws -> [GTCommit]? {
// get the remote origin/master branch
guard let remoteBranches = try storeRepository?.remoteBranches(),
let index = remoteBranches.index(where: { $0.shortName == "master" })
@ -644,22 +710,6 @@ class PasswordStore {
//print("remoteMasterBranch \(remoteMasterBranch)")
// get a list of local commits
if let localCommits = try storeRepository?.localCommitsRelative(toRemoteBranch: remoteMasterBranch),
localCommits.count > 0 {
// get the oldest local commit
guard let firstLocalCommit = localCommits.last,
firstLocalCommit.parents.count == 1,
let newHead = firstLocalCommit.parents.first else {
throw NSError(domain: "me.mssun.pass.error", code: 1, userInfo: [NSLocalizedDescriptionKey: "Cannot decide how to reset."])
}
try self.storeRepository?.reset(to: newHead, resetType: GTRepositoryResetType.hard)
self.updatePasswordEntityCoreData()
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
NotificationCenter.default.post(name: .passwordStoreChangeDiscarded, object: nil)
self.setAllSynced()
return localCommits.count
} else {
return 0 // no new commit
}
return try storeRepository?.localCommitsRelative(toRemoteBranch: remoteMasterBranch)
}
}

View file

@ -23,6 +23,6 @@ class ContentTableViewCell: UITableViewCell {
return nil
}
func setContent(content: String) { }
func setContent(content: String?) { }
}

View file

@ -9,7 +9,7 @@
import UIKit
protocol FillPasswordTableViewCellDelegate {
func generatePassword() -> String
func generateAndCopyPassword()
}
class FillPasswordTableViewCell: ContentTableViewCell {
@ -20,6 +20,7 @@ class FillPasswordTableViewCell: ContentTableViewCell {
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
contentTextField.font = UIFont(name: Globals.passwordFonts, size: (contentTextField.font?.pointSize)!)
}
override func setSelected(_ selected: Bool, animated: Bool) {
@ -29,16 +30,19 @@ class FillPasswordTableViewCell: ContentTableViewCell {
}
@IBAction func generatePassword(_ sender: UIButton) {
let plainPassword = self.delegate?.generatePassword() ?? Utils.generatePassword(length: 16)
contentTextField.attributedText = Utils.attributedPassword(plainPassword: plainPassword)
Utils.copyToPasteboard(textToCopy: plainPassword)
self.delegate?.generateAndCopyPassword()
}
// re-color
@IBAction func textFieldDidChange(_ sender: UITextField) {
contentTextField.attributedText = Utils.attributedPassword(plainPassword: sender.text ?? "")
}
override func getContent() -> String? {
return contentTextField.attributedText?.string
}
override func setContent(content: String) {
contentTextField.attributedText = Utils.attributedPassword(plainPassword: content)
override func setContent(content: String?) {
contentTextField.attributedText = Utils.attributedPassword(plainPassword: content ?? "")
}
}

View file

@ -27,6 +27,9 @@
<nil key="textColor"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" keyboardType="alphabet"/>
<connections>
<action selector="textFieldDidChange:" destination="KGk-i7-Jjw" eventType="editingChanged" id="U0t-2B-JxY"/>
</connections>
</textField>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" reversesTitleShadowWhenHighlighted="YES" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="hTh-ek-Xam">
<rect key="frame" x="243" y="-0.5" width="64" height="89.5"/>

View file

@ -19,15 +19,13 @@ class LabelTableViewCell: UITableViewCell {
@IBOutlet weak var contentLabel: UILabel!
@IBOutlet weak var titleLabel: UILabel!
let passwordStore = PasswordStore.shared
var isPasswordCell = false
var isURLCell = false
var isReveal = false
var isHOTPCell = false
let passwordDots = "••••••••••••"
weak var passwordTableView : PasswordDetailTableViewController?
weak var delegatePasswordTableView : PasswordDetailTableViewController?
var cellData: LabelTableViewCellData? {
didSet {
@ -36,14 +34,14 @@ class LabelTableViewCell: UITableViewCell {
if isReveal {
contentLabel.attributedText = Utils.attributedPassword(plainPassword: cellData?.content ?? "")
} else {
contentLabel.text = passwordDots
contentLabel.text = Globals.passwordDots
}
contentLabel.font = UIFont(name: "Menlo", size: contentLabel.font.pointSize)
contentLabel.font = UIFont(name: Globals.passwordFonts, size: contentLabel.font.pointSize)
} else if isHOTPCell {
if isReveal {
contentLabel.text = cellData?.content ?? ""
} else {
contentLabel.text = passwordDots
contentLabel.text = Globals.passwordDots
}
} else {
contentLabel.text = cellData?.content
@ -78,9 +76,9 @@ class LabelTableViewCell: UITableViewCell {
}
if isHOTPCell {
if isReveal {
return action == #selector(copy(_:)) || action == #selector(LabelTableViewCell.concealPassword(_:)) || action == #selector(LabelTableViewCell.nextPassword(_:))
return action == #selector(copy(_:)) || action == #selector(LabelTableViewCell.concealPassword(_:)) || action == #selector(LabelTableViewCell.getNextHOTP(_:))
} else {
return action == #selector(copy(_:)) || action == #selector(LabelTableViewCell.revealPassword(_:)) || action == #selector(LabelTableViewCell.nextPassword(_:))
return action == #selector(copy(_:)) || action == #selector(LabelTableViewCell.revealPassword(_:)) || action == #selector(LabelTableViewCell.getNextHOTP(_:))
}
}
return action == #selector(copy(_:))
@ -104,48 +102,17 @@ class LabelTableViewCell: UITableViewCell {
}
func concealPassword(_ sender: Any?) {
contentLabel.text = passwordDots
contentLabel.text = Globals.passwordDots
isReveal = false
}
func nextPassword(_ sender: Any?) {
guard let password = passwordTableView?.password,
let passwordEntity = passwordTableView?.passwordEntity else {
print("Cannot find password/passwordEntity of a cell")
return;
}
// increase HOTP counter
password.increaseHotpCounter()
// only the HOTP password needs update
if let plainPassword = password.otpToken?.currentPassword {
cellData?.content = plainPassword
// contentLabel will be updated automatically
}
// commit
if password.changed {
DispatchQueue.global(qos: .userInitiated).async {
self.passwordStore.update(passwordEntity: passwordEntity, password: password, progressBlock: {_ in })
DispatchQueue.main.async {
passwordEntity.synced = false
self.passwordStore.saveUpdated(passwordEntity: passwordEntity)
// reload so that the "unsynced" symbol could be added
self.passwordTableView?.tableView.reloadRows(at: [IndexPath(row: 0, section: 0)], with: UITableViewRowAnimation.automatic)
SVProgressHUD.showSuccess(withStatus: "Password Copied\nCounter Updated")
SVProgressHUD.dismiss(withDelay: 1)
}
}
}
func openLink(_ sender: Any?) {
// if isURLCell, passwordTableView should not be nil
delegatePasswordTableView!.openLink()
}
func openLink(_ sender: Any?) {
guard let password = passwordTableView?.password else {
print("Cannot find password of a cell")
return;
}
Utils.copyToPasteboard(textToCopy: password.password)
UIApplication.shared.open(URL(string: cellData!.content)!, options: [:], completionHandler: nil)
func getNextHOTP(_ sender: Any?) {
// if isHOTPCell, passwordTableView should not be nil
delegatePasswordTableView!.getNextHOTP()
}
}

View file

@ -9,15 +9,21 @@
import UIKit
protocol PasswordSettingSliderTableViewCellDelegate {
func generateAndCopyPassword()
}
class SliderTableViewCell: ContentTableViewCell {
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var valueLabel: UILabel!
@IBOutlet weak var slider: UISlider!
var delegate: UITableViewController?
var roundedValue: Int {
get {
return Int(slider.value)
return Int(valueLabel.text!)!
}
}
@ -33,9 +39,17 @@ class SliderTableViewCell: ContentTableViewCell {
}
@IBAction func handleSliderValueChange(_ sender: UISlider) {
let roundedValue = round(sender.value)
sender.value = roundedValue
valueLabel.text = "\(Int(roundedValue))"
let oldRoundedValue = self.roundedValue
let newRoundedValue = Int(sender.value)
// proceed only when the rounded value gets updated
guard newRoundedValue != oldRoundedValue else {
return;
}
sender.value = Float(newRoundedValue)
valueLabel.text = "\(newRoundedValue)"
if let delegate: PasswordSettingSliderTableViewCellDelegate = self.delegate as? PasswordSettingSliderTableViewCellDelegate {
delegate.generateAndCopyPassword()
}
}
func reset(title: String, minimumValue: Int, maximumValue: Int, defaultValue: Int) {

View file

@ -13,20 +13,20 @@
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="71" id="KGk-i7-Jjw" customClass="SliderTableViewCell" customModule="pass" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="320" height="71"/>
<rect key="frame" x="0.0" y="0.0" width="320" height="74"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
<rect key="frame" x="0.0" y="0.0" width="320" height="70.5"/>
<rect key="frame" x="0.0" y="0.0" width="320" height="73.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="0.5" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="MwT-Jl-hhE">
<rect key="frame" x="60.5" y="20.5" width="205.5" height="31"/>
<rect key="frame" x="60.5" y="22" width="205.5" height="31"/>
<connections>
<action selector="handleSliderValueChange:" destination="KGk-i7-Jjw" eventType="valueChanged" id="WwM-ZE-yIB"/>
</connections>
</slider>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="88" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="GJP-Fj-VZt" userLabel="Value">
<rect key="frame" x="281" y="8" width="24" height="54.5"/>
<rect key="frame" x="281" y="8" width="24" height="57.5"/>
<constraints>
<constraint firstAttribute="width" constant="24" id="tOG-yp-eFw"/>
</constraints>
@ -35,7 +35,7 @@
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="t7T-HC-hUd" userLabel="Title">
<rect key="frame" x="15" y="8" width="30" height="54.5"/>
<rect key="frame" x="15" y="8" width="30" height="57.5"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="38" id="8Tz-Qo-Mkg"/>
</constraints>

View file

@ -23,7 +23,7 @@ class TextFieldTableViewCell: ContentTableViewCell {
override func getContent() -> String? {
return contentTextField.text
}
override func setContent(content: String) {
override func setContent(content: String?) {
contentTextField.text = content
}
}

View file

@ -22,7 +22,7 @@ class TextViewTableViewCell: ContentTableViewCell {
return contentTextView.text
}
override func setContent(content: String) {
override func setContent(content: String?) {
contentTextView.text = content
}
}

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="12106.1" systemVersion="16E154a" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="12106.1" systemVersion="16E189a" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
@ -20,10 +20,10 @@
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="xHX-Sh-1pR">
<rect key="frame" x="15" y="8" width="297" height="200.5"/>
<rect key="frame" x="15" y="15" width="297" height="186.5"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="160" id="Tvq-j8-Nvh"/>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="120" id="Tvq-j8-Nvh"/>
</constraints>
<fontDescription key="fontDescription" name="Menlo-Regular" family="Menlo" pointSize="14"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences" autocorrectionType="no"/>
@ -32,8 +32,8 @@
<constraints>
<constraint firstAttribute="trailingMargin" secondItem="xHX-Sh-1pR" secondAttribute="trailing" id="LWS-JW-9dS"/>
<constraint firstItem="xHX-Sh-1pR" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leadingMargin" constant="7" id="SRq-7t-Gyr"/>
<constraint firstAttribute="bottomMargin" secondItem="xHX-Sh-1pR" secondAttribute="bottom" id="UPQ-jk-QJR"/>
<constraint firstItem="xHX-Sh-1pR" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="topMargin" id="gwb-2C-4wp"/>
<constraint firstAttribute="bottomMargin" secondItem="xHX-Sh-1pR" secondAttribute="bottom" constant="7" id="UPQ-jk-QJR"/>
<constraint firstItem="xHX-Sh-1pR" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="topMargin" constant="7" id="gwb-2C-4wp"/>
</constraints>
</tableViewCellContentView>
<connections>