├── .travis.yml ├── README.md ├── about.go ├── account.go ├── dialog ├── confirm.go └── emptysearch.go ├── doc ├── account.png ├── decryptingtoken.png ├── file.png ├── otp.png ├── search.png ├── sign.png └── unlock.png ├── file.go ├── lock ├── aead.go ├── aead_test.go ├── lock.go └── lock_test.go ├── mac.json ├── main.go ├── menu.go ├── otp.go ├── otp └── otp.go ├── resources ├── buttons │ ├── arrow-down.png │ ├── arrow-up.png │ ├── check.png │ ├── dice.png │ ├── history.png │ ├── signfile.png │ ├── times.png │ └── trash.png ├── css │ ├── pass.css │ └── pass.css.map ├── icon.png ├── iconpack │ ├── addthis500.png │ ├── app-net.png │ ├── behance.png │ ├── blogger.png │ ├── default.png │ ├── deviantart.png │ ├── digg.png │ ├── dribbble.png │ ├── etsy.png │ ├── facebook.png │ ├── file.png │ ├── flickr.png │ ├── foursquare.png │ ├── github.png │ ├── gmail.png │ ├── googleplus.png │ ├── grocid.net.png │ ├── hangouts.png │ ├── indiegogo.png │ ├── instagram.png │ ├── kickstarter.png │ ├── lastfm.png │ ├── linkedin.png │ ├── livejournal.png │ ├── meetup.png │ ├── messenger.png │ ├── mixcloud.png │ ├── myspace.png │ ├── orkut.png │ ├── otp.png │ ├── paypal.png │ ├── picasa.png │ ├── pinterest.png │ ├── reddit.png │ ├── rss.png │ ├── sharethis.png │ ├── sign.png │ ├── skype.png │ ├── soundcloud.png │ ├── spotify.png │ ├── stackoverflow.png │ ├── steam.png │ ├── stumbleupon.png │ ├── tumblr.png │ ├── twitter.png │ ├── vimeo.png │ ├── vk.png │ ├── yelp.png │ ├── youtube-variation.png │ └── youtube.png └── symbols │ ├── key_small.png │ ├── key_small_dark.png │ ├── lock.png │ ├── lock_dark.png │ ├── port_small.png │ ├── search.png │ ├── search_small.png │ ├── search_small_dark.png │ ├── server_small.png │ ├── trash.png │ ├── trash_dark.png │ ├── user_small.png │ └── user_small_dark.png ├── rest ├── encoding.go ├── rest.go └── rest_test.go ├── search.go ├── sign.go ├── unlock.go └── util └── util.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | os: osx 3 | go: 4 | - master 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Travis](https://travis-ci.org/grocid/PassMacOS.svg?branch=master) 2 | 3 | # Pass Desktop for macOS 4 | 5 | Pass Desktop is a GUI for [pass](https://github.com/grocid/pass), but completely independent of it. It communicates with [Hashicorp Vault](https://www.vaultproject.io) (from now on called just Vault), where all accounts with associated usernames and passwords are stored. Any instance of Vault can be used, no additional setup. So if you are already running Vault, just generate a token and you are ready to go. 6 | 7 | So, why have I decided to use Vault as the storage? Vault is widely used software for storing secrets and exists in many production environments. It is reasonably fast (I would say, about as fast a normal database). Pass Desktop should be a plug-and-play experience. Nonetheless, if one would like to use a different kind of storage, rewriting the operations to another database is smooth sailin' ;-) 8 | 9 | ### Encrypted data 10 | 11 | Pass is password protected on the local computer, by storing an `encrypted token` on disk, along with a `nonce` and `salt`. The `token` is encrypted with ChaCha20-Poly1305 (for which there are basically no attacks at this date). The encryption key is derived as 12 | 13 | ```go 14 | key := Argon2(password, salt, *params) 15 | ``` 16 | and 17 | 18 | ```go 19 | token := ChaCha20-Poly1305-Decrypt(encrypted token, key, nonce). 20 | ``` 21 | 22 | The decrypted `token` is kept in memory only. Apart from that, it is actually agnostic to the underlying data storage. Therefore, all entries are encrypted with ChaCha20-Poly1305-Encrypt, under the same key as the token (but of course, different nonces). Although, in the case of Vault the database in encrypted with a AES-GCM barrier and protected with some additional security mechanisms such as token access and secret sharing. 23 | 24 | When you have configured Pass and you start it. You will be presented with the following screen, which requires you to input your password. 25 | 26 | Inside Vault, the entries have the following format. 27 | 28 | ``` 29 | { 30 | "" : "", 31 | "data": { 32 | "encrypted": " 33 | ----------------ENCRYPTED---------------- <-- not actual data 34 | { 35 | "username": "grocid", 36 | "password": "banana", 37 | "file": [bytes], 38 | "padding": "..." 39 | } 40 | ----------------------------------------- <-- not actual data 41 | " 42 | } 43 | } 44 | 45 | ``` 46 | 47 | The `padding` is a random string which pads the encrypted data to a minimum length. This to make sure no useable information is leaked (e.g. if your password happens to be very short, then it may be reflected in the length of the ciphertext). Large files are identifiable as files, of course, by just looking at the ciphertext. The account name is also encrypted, in case you do not want to leak which sites you are registered on (provided the improbable scenario that your token gets stolen but not your salt and password, or that your server provider is malicious and is able to intercept the unseal keys to your Vault instance). 48 | 49 | ![Unlock](doc/unlock.png) 50 | 51 | After unlocking the token, the search view will be available to you: 52 | 53 | ![Search](doc/search.png) 54 | 55 | Below is a screenshot from the account view. There is a possibility to generate passwords at random by pressing the die. If you accidentially press it, just press the cancel (X) button. 56 | 57 | ![Account](doc/account.png) 58 | 59 | In terms of Vault, the get request for a specific secret, let us say Github, would be something like 60 | 61 | ```sh 62 | GET /secret/6bb5d1af6cf022c8df559a1b4b0217c92d4e33ffd20abd72865dcccf 63 | ``` 64 | 65 | Pass perfoms, at every query, real-time decryption of the content. No data is explicitly stored on disk. 66 | 67 | ![Decrypting token](doc/decryptingtoken.png) 68 | 69 | ### OTP 70 | 71 | Vault has an option for OTP, in which case the shared secret is stored inside Vault. The drawback here is that it will rely only on the AES-GCM wall, which of course might be enough. On the other hand, if the security of the server is comprimised any such data will leak. 72 | 73 | As a consequence, Pass stores the shared secret in Vault as a normal entry (which is encrypted with the local key, not known to the server). The shared secret is fetched and then Pass generates the OTP locally using the encryption keys. Note that (this sort of) takes the purpose out of multi-factor authentication and should therefore be used conservatively. Please be aware of this. 74 | 75 | 76 | ![OTP](doc/otp.png) 77 | 78 | 79 | ### Files 80 | 81 | It is possible to store files in the same way as user credentials are stored. Files are then downloaded and decrypted on the local computer. Due to limitations in Vault, the maximum file size is 512 kB. 82 | 83 | ![File](doc/file.png) 84 | 85 | 86 | ### Signing keys 87 | 88 | Pass has an option to generate Ed25519 key pairs for signing and verification. To sign a file, simply press the signing button (marked in the screenshot below) and select a file. A new file containing the signature will be generated, with the file ending `.signature`. 89 | 90 | ![Sign](doc/sign.png) 91 | 92 | The Base64-encoded text shown above is your public key. It can be distributed and is used to verify generated signatures. To avoid mistakes, the private key cannot be exported. 93 | 94 | ### Other capabilites 95 | 96 | It is pretty easy to implement another type of entry. If you want feature X, look at any implemented type. 97 | 98 | ## Seurity considerations 99 | 100 | - To use mutual authentication. This requires each accessor/user to have a valid private key. Private key can be encrypted with master password. 101 | - Using [fw](https://github.com/grocid/fw) to only allow white-listed users. Requires the user to authenticate with Google Authenticator to white list its IP address. Makes it harder for attackers, but does not yield any real security. 102 | - The ideal solution would be to use an OTP such as Google Authenticator to authenticate directly to Vault (and not fw), which would give a session last for a certain amount of time. So, even if your laptop gets stolen and extremely short password is determined by bruteforce, you would still need an OTP to gain access (unless the session still is valid). 103 | - (*Slightly deprecated*) To use root token or regular tokens: when sharing a server with multiple users and associated (disjoint) storage areas, different tokens are needed and, hence, root token cannot be used. In even in single-user mode, use of root token is not recommended. 104 | - (*Slightly deprecated*) Trusting a third-party server. The holder of the root token (or a group/individual holding of the unseal keys) will be able to read all data stored in Vault. However, Vault is quite light weight and can, for a limited amount of users, be run on a mere Raspberry Pi gen A. I would suggest that each user runs Vault on their own Raspberry Pi at home. Secrets can be shared over several VPS instances and providers using secret sharing. While at a higher cost, it would give higher security and accessibility (as a e.g. (3, 2) scheme would require only two out of three servers to be online). 105 | - If the password has a lot lower entropy than 256 bits, then the iteration count / Argon2 parameters need to be increased considerably if you are planning on leaking your config file. 106 | - No communication with other clients, only authenticated servers. 107 | 108 | ### Why use two encryption layers? 109 | 110 | It may seem unncessary to encrypt data twice. If you are running a secured instance of Vault -- then probably -- yes. What comes into play here is the distribution of cryptographic keys. If you are running a pure Vault setup, then your server knows all the secrets. You will not be able to read data from disk, since Vault does in-memory decryption, but if you get hold of a token or a memory dump or can leak memory using recent attacks, then your data is in trouble. 111 | 112 | ### Possible leaks 113 | 114 | When an Apple computer goes into hibernation (not regular sleep), in-memory contents are transferred to disk. The Filevault key itself can be recovered when device is in sleep (default for desktops), or deep/hybrid sleep, or whatever they call it (default for laptops), so presumably the token is also somewhere. The difference (in description) between hybrid and full hibernation is only that memory power is disconnected, so it might mean that in the hibernation file you also have all keys. And there is also a, notably non-standard, option to wipe the Filevault key each time the system goes to standby. By default Apple do not wipe the key -- this is usability consideration -- user gets faster response from system and no annoying passwords are needed. 115 | 116 | There is the ```pmset somethingVaultKeysomethingsomething``` setting. If you are concerned about this, I suggest you do some own research. 117 | 118 | What about Spectre and Meltdown? Pass Desktop is agnostic to these attacks. If the operating system is vulnerable, your memory will leak no matter what. 119 | 120 | ### Can I connect with other users? 121 | 122 | Although it would be nice to create shared areas in Vault (e.g. for sharing a Netflix account), there is no such option as of now. Direct communcation between clients is advised against, since that is a quite servere attack vector. The client will only talk to an authenticated server. If the server is overtaken by a malicious party then it could -- potentially -- inject data. This may or may not pose a threat, in case there is some bug in the REST implementation or in Golangs libraries. 123 | 124 | ### How to sync between devices 125 | 126 | If you created an App with macpack, then you can simply copy the App, because the configuration file will be included. It is therefore totally inappropriate to distribute the App. 127 | 128 | If you build pass in another way, the file `config.json` needs to be copied to the target computer. Even though the token in the configuration file is encrypted, I suggest not storing the it on insecure media like Dropbox or unencrypted mail. 129 | 130 | 131 | ## Performance 132 | 133 | Pass Desktop keeps no information stored on disk. Search operations are done by performing a `LIST` (Hashicorp-specific operation), which fetches a JSON with all keys (account names) from the server, after which decryption and filtering operations are performed locally. 134 | 135 | ### Minimizing query complexity 136 | 137 | Every time a `PUT` or `DELETE` is invoked, it will simultaneously update a tag (random value) on the path `/secret/updated`, but also a local variable `LocalUpdate`. When performing a search, it will check whether if `LocalUpdate == true` (in case the modifying operation came from the local client). If not, it will check if `/secret/updated` matches a locally stored value. If `LocalUpdate` was set or the tags dont match, Pass will fetch the contents from the server. This, to avoid fetching already known data. This makes the less common `PUT` and `DELETE` twice as expensive in terms of requests made, but as a trade-off, searching large lists will be much less expensive. 138 | 139 | ### Graphics 140 | 141 | Moreever, Pass Desktop keeps an iconset, where each filename is associated with the account name (favicons are too small). Since there is a mapping betwen account names and the iconset, the recommended convention is to name accounts after the domain. The iconset can be extended by the user with minor effort. The memory usage is about 50 MBs of RAM. 142 | 143 | ## Building 144 | 145 | It is as simple as 146 | 147 | ```sh 148 | go build 149 | ``` 150 | 151 | which creates a standalone executable. To build a real .App, I suggest using [macpack](https://github.com/murlokswarm/macpack). This is equally simple: 152 | 153 | ```sh 154 | macpack build 155 | ``` 156 | 157 | The application will try to load your certificate authority (CA), located as an entry inside `config.json`. 158 | The CA will be used to authenticate the server you are running Vault on (we do not really need anything else than a self-signed certificate). When you setup your server, you generated a CA. This is the file you need. 159 | The configuration `config.json` is a file of the format 160 | 161 | ```json 162 | { 163 | "encrypted": { 164 | "token": "...", 165 | "salt": "..." 166 | }, 167 | "host": "myserver.com", 168 | "port": "8001", 169 | "ca": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----" 170 | } 171 | ``` 172 | 173 | ## Setting up the backend 174 | 175 | To get Pass working, you need to install and configure Vault on the remote server. First, start the storage backend for Vault. This can be SQL, but I would recommend [Consul](https://www.consul.io). Start Consul as follows: 176 | 177 | ```sh 178 | consul agent -server -config-dir=/etc/consul.d/bootstrap/ > /dev/null 2>&1 & 179 | ``` 180 | 181 | Let the contents of `/etc/consul.d/bootstrap/config.json` be 182 | 183 | ```json 184 | { 185 | "bootstrap": true, 186 | "server": true, 187 | "datacenter": "pass", 188 | "data_dir": "/var/consul", 189 | "encrypt": "", 190 | "ca_file": "/etc/consul.d/ssl/ca.crt", 191 | "cert_file": "/etc/consul.d/ssl/consul.crt", 192 | "key_file": "/etc/consul.d/ssl/consul.key", 193 | "verify_incoming": true, 194 | "verify_outgoing": true, 195 | "log_level": "INFO", 196 | "enable_syslog": false 197 | } 198 | ``` 199 | 200 | Then, Vault can be started in the following way. 201 | 202 | ```sh 203 | vault server -config=/etc/vault.d/config.json > /dev/null 2>&1 & 204 | ``` 205 | 206 | where `/etc/vault.d/config.json` contains 207 | 208 | ```json 209 | { 210 | "storage": { 211 | "consul": { 212 | "address": "127.0.0.1:8500", 213 | "advertise_addr": "https://127.0.0.1:8200", 214 | "path": "vault" 215 | } 216 | }, 217 | "listener": { 218 | "tcp": { 219 | "address": "127.0.0.1:8200", 220 | "tls_cert_file": "/etc/vault.d/ssl/vault.crt", 221 | "tls_key_file": "/etc/vault.d/ssl/vault.key", 222 | "tls_disable": 0 223 | } 224 | } 225 | } 226 | ``` 227 | 228 | Vault binds to `127.0.0.1`, so we need fw to access it (if you do not want to use fw, then bind the listener to `0.0.0.0:8001`). We can start it as 229 | 230 | ```sh 231 | ./fw 0.0.0.0:8001 127.0.0.1:8200 2>&1 & 232 | ``` 233 | 234 | The more proper way to start Consul, Vault and fw would be to create an init.d or systemd service, but this is fine for testing purposes. 235 | 236 | ## Icon pack 237 | 238 | [Somacro](http://veodesign.com/2011/en/11/08/somacro-27-free-big-and-simple-social-media-icons/) -------------------------------------------------------------------------------- /about.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/murlokswarm/app" 5 | ) 6 | 7 | type About struct{} 8 | 9 | func (*About) Render() string { 10 | return ` 11 |
12 |
13 |
17 | 19 |

Pass Desktop

20 |

21 | Written by Carl Löndahl
grocid.net 22 |

23 |

24 | Pass Desktop is free software and licensed under the BSD three-clause (revised) license. 25 |

26 |

27 | Copyright © 2018 Carl Löndahl. All rights reserved. 28 |

29 |
30 |
31 |
33 |
34 |
` 35 | } 36 | 37 | func (h *About) OK() { 38 | if !pass.Locked { 39 | // If it was unlocked, we can go back to search... 40 | NavigateBack("") 41 | } else { 42 | // otherwise, we need to return to unlock screen. 43 | s := UnlockScreen{} 44 | win.Mount(&s) 45 | } 46 | } 47 | 48 | func init() { 49 | app.RegisterComponent(&About{}) 50 | } 51 | -------------------------------------------------------------------------------- /account.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Carl Löndahl. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Pass Desktop nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package main 32 | 33 | import ( 34 | "github.com/murlokswarm/app" 35 | "log" 36 | "net/url" 37 | //"pass/dialog" 38 | "pass/lock" 39 | "pass/rest" 40 | ) 41 | 42 | type Account struct { 43 | //Deletable 44 | Title string 45 | ImageName string 46 | Query string 47 | Data rest.DecodedEntry 48 | } 49 | 50 | func (h *Account) DefaultView() string { 51 | return ` 52 |
53 |
54 | 66 |
67 |
71 | 73 |

84 |
85 |
96 | 107 |
108 |
109 |
110 |
115 |
116 |
117 |
118 | ` 119 | } 120 | func (h *Account) Render() string { 121 | return h.DefaultView() 122 | } 123 | 124 | func (h *Account) OnHref(URL *url.URL) { 125 | // Extract information from query and get account name and 126 | // its encrypted counterpart from query (this is need since 127 | // if we were to encrypt again, we would get a different 128 | // encrypted name). 129 | u := URL.Query() 130 | h.Title = u.Get("Name") 131 | 132 | restResponse, err := restClient.VaultReadSecret( 133 | &rest.Name{ 134 | Text: h.Title, 135 | Encrypted: u.Get("Encrypted"), 136 | }) 137 | 138 | if err != nil { 139 | log.Println(err) 140 | return 141 | } 142 | 143 | h.Data = *restResponse 144 | 145 | // Acquire the image name. If it exists in preloaded map, 146 | // use it as is, but if it is not, we subsitute. 147 | h.ImageName = GetImageName(h.Title) 148 | 149 | //log.Println(h.Render()) 150 | 151 | // Tells the app to update the rendering of the component. 152 | app.Render(h) 153 | } 154 | 155 | func (h *Account) OK() { 156 | // We do not want empty names. 157 | if h.Title == "" { 158 | return 159 | } 160 | 161 | d := h.Data.Name 162 | if d != nil && h.Title != (*d).Text { 163 | // need to remove old and submit new 164 | } else { 165 | if d == nil { 166 | h.Data.Name = &rest.Name{ 167 | Text: h.Title, 168 | } 169 | } 170 | // Modify the decoded entry so that it matches 171 | //the contents of the UI. 172 | err := restClient.VaultWriteSecret(&h.Data) 173 | 174 | if err != nil { 175 | log.Println(err) 176 | return 177 | } 178 | } 179 | // Now, we just need to go back. 180 | h.Cancel() 181 | } 182 | 183 | func (h *Account) Cancel() { 184 | NavigateBack("") 185 | } 186 | 187 | func (h *Account) RandomizePassword() { 188 | // Generate a new password. 189 | h.Data.Password = lock.EntropyAlphabet(32) 190 | 191 | // Tells the app to update the rendering of the component. 192 | app.Render(h) 193 | } 194 | 195 | func (h *Account) Delete() { 196 | d := h.Data.Name 197 | if d != nil { 198 | restClient.VaultDeleteSecret(&h.Data) 199 | } 200 | h.Cancel() 201 | } 202 | 203 | func (h *Account) DoSearchQuery(arg app.ChangeArg) { 204 | log.Println(arg.Value, h.Query) 205 | NavigateBack(arg.Value) 206 | } 207 | 208 | func init() { 209 | app.RegisterComponent(&Account{}) 210 | } 211 | -------------------------------------------------------------------------------- /dialog/confirm.go: -------------------------------------------------------------------------------- 1 | package dialog 2 | 3 | func Confirm() string { 4 | return ` 5 |
6 |
7 | 17 |
18 |
19 |
20 |
21 |
24 |
25 |
` 26 | } 27 | -------------------------------------------------------------------------------- /dialog/emptysearch.go: -------------------------------------------------------------------------------- 1 | package dialog 2 | 3 | func EmptySearch() string { 4 | return ` 5 |
6 |
7 | 17 |
18 |
19 | 24 |
` 25 | } 26 | -------------------------------------------------------------------------------- /doc/account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/doc/account.png -------------------------------------------------------------------------------- /doc/decryptingtoken.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/doc/decryptingtoken.png -------------------------------------------------------------------------------- /doc/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/doc/file.png -------------------------------------------------------------------------------- /doc/otp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/doc/otp.png -------------------------------------------------------------------------------- /doc/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/doc/search.png -------------------------------------------------------------------------------- /doc/sign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/doc/sign.png -------------------------------------------------------------------------------- /doc/unlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/doc/unlock.png -------------------------------------------------------------------------------- /file.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/sha1" // for non-cryptographic purposes 5 | "encoding/hex" 6 | "fmt" 7 | "github.com/murlokswarm/app" 8 | "io/ioutil" 9 | "log" 10 | "net/url" 11 | "os/user" 12 | "pass/rest" 13 | ) 14 | 15 | type File struct { 16 | Title string 17 | Query string 18 | Changed bool 19 | Data rest.DecodedEntry 20 | } 21 | 22 | func (h *File) Render() string { 23 | // Calculate file size in kBs. 24 | fs := fmt.Sprintf("%.2f", float64(len(h.Data.File))/1024) 25 | return ` 26 |
27 |
28 | 38 |
39 |
43 | ` + GetFingerprint(h.Data.File, 255, 255, 255) + ` 44 |

{{.Title}}

45 |
46 |

Size

47 |

` + fs + ` kB

48 |
49 |
50 |
51 |
56 |
57 |
58 |
` 59 | 60 | } 61 | 62 | func GetFingerprint(data []byte, r int, g int, b int) string { 63 | // Generate a hash from file. This is only for visualization, 64 | // and not for cryptographic purposes. Information leak through 65 | // fingerprint image is negligible. 66 | hashFunction := sha1.New() 67 | hashFunction.Write(data) 68 | hashDigest := hashFunction.Sum(nil) 69 | 70 | // Generate a grid programatically. 71 | grid := `
` 72 | for i := 0; i < 16; i++ { 73 | item := fmt.Sprintf(`
75 |
`, r, g, b, 10*int(hashDigest[i])/255) 76 | grid = grid + item 77 | } 78 | 79 | return grid + `
` 80 | } 81 | 82 | func (h *File) OnHref(URL *url.URL) { 83 | // Extract information from query and get account name and 84 | // its encrypted counterpart from query (this is need since 85 | // if we were to encrypt again, we would get a different 86 | // encrypted name). 87 | u := URL.Query() 88 | h.Title = u.Get("Name") 89 | restResponse, err := restClient.VaultReadSecret( 90 | &rest.Name{ 91 | Text: h.Title, 92 | Encrypted: u.Get("Encrypted"), 93 | }) 94 | 95 | if err != nil { 96 | log.Println(err) 97 | return 98 | } 99 | 100 | h.Data = *restResponse 101 | 102 | // Tells the app to update the rendering of the component. 103 | app.Render(h) 104 | } 105 | 106 | func (h *File) OK() { 107 | // Make sure we do not save already saved information. 108 | if h.Changed { 109 | // No empy names. 110 | if h.Title == "" { 111 | return 112 | } 113 | 114 | d := h.Data.Name 115 | if h.Title != (*d).Text { 116 | // need to remove old and submit new 117 | } else { 118 | // Modify the decoded entry so that it matches 119 | //the contents of the UI. 120 | restClient.VaultWriteSecret(&h.Data) 121 | } 122 | } 123 | 124 | // Now, we just need to go back. 125 | h.Cancel() 126 | } 127 | 128 | func (h *File) Cancel() { 129 | NavigateBack("") 130 | } 131 | 132 | func (h *File) SaveFile() { 133 | usr, err := user.Current() 134 | 135 | if err != nil { 136 | log.Fatal(err) 137 | } 138 | 139 | hashFunction := sha1.New() 140 | hashFunction.Write(h.Data.File) 141 | hexDigest := hex.EncodeToString(hashFunction.Sum(nil)) 142 | 143 | filename := usr.HomeDir + "/Downloads/" + hexDigest 144 | ioutil.WriteFile(filename, h.Data.File, 0644) 145 | 146 | log.Println("Wrote file" + filename) 147 | 148 | app.Render(h) 149 | } 150 | 151 | func (h *File) ReadFile() { 152 | // Open filepicker window to get filename. 153 | app.NewFilePicker(app.FilePicker{ 154 | MultipleSelection: false, 155 | NoDir: true, 156 | NoFile: false, 157 | OnPick: func(filenames []string) { 158 | // Get contents of file. 159 | b, err := ioutil.ReadFile(filenames[0]) 160 | 161 | // If there was an error, probably due 162 | // to permissions, dump error message 163 | // to log. 164 | if err != nil { 165 | log.Println(err) 166 | } else { 167 | h.Data.File = b 168 | h.Changed = true 169 | } 170 | 171 | app.Render(h) 172 | }, 173 | }) 174 | } 175 | 176 | func (h *File) Delete() { 177 | d := h.Data.Name 178 | if d != nil { 179 | restClient.VaultDeleteSecret(&h.Data) 180 | } 181 | h.Cancel() 182 | } 183 | 184 | func (h *File) DoSearchQuery(arg app.ChangeArg) { 185 | NavigateBack(arg.Value) 186 | } 187 | 188 | func init() { 189 | app.RegisterComponent(&File{}) 190 | } 191 | -------------------------------------------------------------------------------- /lock/aead.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Carl Löndahl. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Pass Desktop nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package lock 32 | 33 | import ( 34 | "crypto/rand" 35 | "errors" 36 | "golang.org/x/crypto/chacha20poly1305" 37 | ) 38 | 39 | func Chacha20Poly1305Encrypt(plaintext []byte, key []byte) ([]byte, error) { 40 | // Create a key data structure 41 | chacha20aead, err := chacha20poly1305.New(key) 42 | 43 | if err != nil { 44 | return []byte{}, err 45 | } 46 | 47 | nonce := make([]byte, chacha20poly1305.NonceSize) 48 | rand.Read(nonce) 49 | 50 | ciphertext := chacha20aead.Seal(nil, nonce, plaintext, nil) 51 | 52 | return append(nonce, ciphertext...), nil 53 | } 54 | 55 | func Chacha20Poly1305Decrypt(ciphertext []byte, key []byte) ([]byte, error) { 56 | // Create a key data structure 57 | chacha20aead, err := chacha20poly1305.New(key) 58 | 59 | if err != nil || len(ciphertext) < chacha20poly1305.NonceSize { 60 | return []byte{}, errors.New("failed to decrypt or verify message authentication code") 61 | } 62 | 63 | plaintext, err := chacha20aead.Open(nil, 64 | ciphertext[:chacha20poly1305.NonceSize], 65 | ciphertext[chacha20poly1305.NonceSize:], nil) 66 | 67 | if err != nil { 68 | return []byte{}, err 69 | } 70 | 71 | return plaintext, nil 72 | } 73 | -------------------------------------------------------------------------------- /lock/aead_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Carl Löndahl. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Pass Desktop nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package lock 32 | 33 | import ( 34 | "crypto/rand" 35 | "golang.org/x/crypto/chacha20poly1305" 36 | "testing" 37 | ) 38 | 39 | func TestEncryptAndDecrypt(t *testing.T) { 40 | key := make([]byte, 32) 41 | rand.Read(key) 42 | 43 | plaintext := []byte("this is a test vector") 44 | ciphertext, err := Chacha20Poly1305Encrypt(plaintext, key) 45 | 46 | if err != nil { 47 | t.Errorf("Encryption error") 48 | } 49 | 50 | decryptedCiphertext, err := Chacha20Poly1305Decrypt(ciphertext, key) 51 | 52 | if err != nil { 53 | t.Errorf("Decryption error") 54 | } 55 | 56 | if string(decryptedCiphertext) != string(plaintext) { 57 | t.Errorf("Decryption was incorrect, got: %s, want: %s.", 58 | plaintext, 59 | decryptedCiphertext) 60 | } 61 | } 62 | 63 | func TestRepeatedNonce(t *testing.T) { 64 | key := make([]byte, 32) 65 | rand.Read(key) 66 | 67 | plaintext := []byte("this is a test vector") 68 | 69 | nonces := make(map[string]bool) 70 | 71 | for i := 0; i < 10000; i++ { 72 | ciphertext, _ := Chacha20Poly1305Encrypt(plaintext, key) 73 | 74 | nonce := string(ciphertext[:chacha20poly1305.NonceSize]) 75 | 76 | if nonces[nonce] == true { 77 | t.Errorf("Colliding nonces, should be nearly impossible.") 78 | } 79 | 80 | nonces[nonce] = true 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lock/lock.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Carl Löndahl. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Pass Desktop nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package lock 32 | 33 | import ( 34 | "crypto/rand" 35 | "crypto/sha256" 36 | "encoding/base64" 37 | "encoding/hex" 38 | "golang.org/x/crypto/argon2" 39 | "golang.org/x/crypto/pbkdf2" 40 | "math/big" 41 | ) 42 | 43 | const ( 44 | SaltLength = 32 45 | 46 | PBKDF2Iterations = 4096 47 | Argon2Time = 4 48 | Argon2Memory = 32 * 1024 49 | Argon2Threads = 4 50 | Argon2KeyLen = 32 51 | 52 | UseArgon2ForKeyDerivation = true 53 | DefaultGeneratedPasswordLength = 32 54 | ) 55 | 56 | // The alphabet from which characters are drawn. The entropy is per 57 | // symbol for this alphabet is roughly ~5.95 bits, so the default 58 | // setting (length = 32) generates passwords with ~190 bits. 59 | var alphabet = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") 60 | 61 | type Lock struct { 62 | Key []byte 63 | Salt []byte 64 | } 65 | 66 | func New(password string, salt []byte) Lock { 67 | key := DeriveKey([]byte(password), salt) 68 | l := Lock{ 69 | Key: key, 70 | Salt: salt} 71 | return l 72 | } 73 | 74 | func Entropy(length int) []byte { 75 | r := make([]byte, length) 76 | rand.Read(r) 77 | 78 | return r 79 | } 80 | 81 | func EntropyAlphabet(length int) string { 82 | var result string 83 | 84 | // Create a random password of specified length. 85 | for { 86 | if len(result) >= length { 87 | return result 88 | } 89 | // Read from from a CSPRNG. 90 | num, err := rand.Int(rand.Reader, big.NewInt(int64(len(alphabet)))) 91 | 92 | if err != nil { 93 | return "" 94 | } 95 | // Add from alphabet. 96 | n := num.Int64() 97 | result += string(alphabet[n]) 98 | } 99 | } 100 | 101 | func DeriveKey(password []byte, salt []byte) []byte { 102 | // Setting to specify key derivation algorithm. 103 | if UseArgon2ForKeyDerivation { 104 | // Return derived key from Argon2. 105 | key := argon2.Key(password, salt, 106 | Argon2Time, 107 | Argon2Memory, 108 | Argon2Threads, 109 | Argon2KeyLen) 110 | return key 111 | } else { 112 | // Fallback to PBKDF2. 113 | key := pbkdf2.Key(password, salt, 114 | PBKDF2Iterations, SaltLength, 115 | sha256.New) 116 | return key 117 | } 118 | } 119 | 120 | func (l *Lock) EncryptAndEncodeBase64(plaintext string) ([]byte, error) { 121 | // Encrypt with AEAD. 122 | ciphertext, err := Chacha20Poly1305Encrypt([]byte(plaintext), l.Key) 123 | 124 | // Return base64 encoded 125 | return []byte(base64.StdEncoding.EncodeToString(ciphertext)), err 126 | } 127 | 128 | func (l *Lock) Base64DecodeAndDecrypt(base64Ciphertext []byte) (string, error) { 129 | // Decode the provided base64 string, use strings for simplicity 130 | ciphertext, err := base64.StdEncoding.DecodeString(string(base64Ciphertext)) 131 | 132 | // Use key to decrypt ciphertext 133 | token, err := Chacha20Poly1305Decrypt(ciphertext, l.Key) 134 | return string(token), err 135 | } 136 | 137 | func (l *Lock) EncryptAndEncodeHex(plaintext string) (string, error) { 138 | // Encrypt with AEAD. 139 | ciphertext, err := Chacha20Poly1305Encrypt([]byte(plaintext), l.Key) 140 | 141 | // Return hexencoded 142 | return string(hex.EncodeToString(ciphertext)), err 143 | } 144 | 145 | func (l *Lock) HexDecodeAndDecrypt(hexCiphertext string) (string, error) { 146 | // Decode the provided hex strings 147 | ciphertext, _ := hex.DecodeString(hexCiphertext) 148 | 149 | // Use key to decrypt ciphertext 150 | token, err := Chacha20Poly1305Decrypt(ciphertext, l.Key) 151 | 152 | return string(token), err 153 | } 154 | 155 | func (l *Lock) LockToken(token string) (string, string, error) { 156 | // Generate random salt. 157 | l.Salt = Entropy(SaltLength) 158 | 159 | hexToken, err := l.EncryptAndEncodeHex(token) 160 | 161 | if err != nil { 162 | return "", "", err 163 | } 164 | 165 | hexSalt := hex.EncodeToString(l.Salt) 166 | 167 | // ...and return to caller. 168 | return hexToken, hexSalt, nil 169 | } 170 | 171 | func (l *Lock) UnlockToken(hexToken string) (string, error) { 172 | 173 | token, err := l.HexDecodeAndDecrypt(hexToken) 174 | 175 | // Take care of errors, i.e., if message authentication failed... 176 | if err != nil { 177 | return "", err 178 | } 179 | 180 | // ...otherwise, return to UI 181 | return token, nil 182 | } 183 | -------------------------------------------------------------------------------- /lock/lock_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Carl Löndahl. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Pass Desktop nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package lock 32 | 33 | import ( 34 | "testing" 35 | ) 36 | 37 | func TestLockAndUnlockToken(t *testing.T) { 38 | 39 | token := "this is a test vector" 40 | password := "mypassword" 41 | salt := Entropy(32) 42 | 43 | l := New(password, salt) 44 | 45 | lockedToken, _, err := l.LockToken(token) 46 | 47 | if err != nil { 48 | t.Errorf("Token lock error") 49 | } 50 | 51 | decryptedToken, err := l.UnlockToken(lockedToken) 52 | 53 | if err != nil { 54 | t.Errorf("Token unlock error") 55 | } 56 | 57 | if decryptedToken != token { 58 | t.Errorf("Token was incorrect, got: %s, want: %s.", 59 | token, 60 | decryptedToken) 61 | } 62 | } 63 | 64 | func TestEncryptAndDecryptBase64(t *testing.T) { 65 | 66 | plaintext := "this is a test vector" 67 | password := "mypassword" 68 | salt := Entropy(32) 69 | l := New(password, salt) 70 | 71 | ciphertext, err := l.EncryptAndEncodeBase64(plaintext) 72 | 73 | if err != nil { 74 | t.Errorf("Encryption error") 75 | } 76 | 77 | decryptedCiphertext, err := l.Base64DecodeAndDecrypt(ciphertext) 78 | 79 | if err != nil { 80 | t.Errorf("Decryption error") 81 | } 82 | 83 | if decryptedCiphertext != plaintext { 84 | t.Errorf("Decrypted ciphertext was incorrect, got: %s, want: %s.", 85 | plaintext, 86 | decryptedCiphertext) 87 | } 88 | } 89 | 90 | func TestEncryptAndDecryptHex(t *testing.T) { 91 | 92 | plaintext := "this is a test vector" 93 | password := "mypassword" 94 | salt := Entropy(32) 95 | l := New(password, salt) 96 | 97 | ciphertext, err := l.EncryptAndEncodeHex(plaintext) 98 | 99 | if err != nil { 100 | t.Errorf("Encryption error") 101 | } 102 | 103 | decryptedCiphertext, err := l.HexDecodeAndDecrypt(ciphertext) 104 | 105 | if err != nil { 106 | t.Errorf("Decryption error") 107 | } 108 | 109 | if decryptedCiphertext != plaintext { 110 | t.Errorf("Decrypted ciphertext was incorrect, got: %s, want: %s.", 111 | plaintext, 112 | decryptedCiphertext) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /mac.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Pass", 3 | "id": "org.grocid.net.pass", 4 | "version": "1.0", 5 | "build-number": 1, 6 | "icon": "icon.png", 7 | "dev-region": "en", 8 | "deployment-target": "10.12", 9 | "copyright": "Copyright © 2018 Carl Löndahl. All rights reserved", 10 | "role": "None", 11 | "category": "", 12 | "sandbox": false, 13 | "capabilities": { 14 | "network": { 15 | "in": true, 16 | "out": true 17 | }, 18 | "hardware": { 19 | "camera": false, 20 | "microphone": false, 21 | "usb": false, 22 | "printing": false, 23 | "bluetooth": false 24 | }, 25 | "app-data": { 26 | "contacts": false, 27 | "location": false, 28 | "calendar": false 29 | }, 30 | "file-access": { 31 | "user-selected": "", 32 | "downloads": "", 33 | "pictures": "", 34 | "music": "", 35 | "movies": "" 36 | } 37 | }, 38 | "store": false, 39 | "sign-id": "", 40 | "supported-files": [] 41 | } 42 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Carl Löndahl. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Pass Desktop nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package main 32 | 33 | import ( 34 | "github.com/murlokswarm/app" 35 | _ "github.com/murlokswarm/mac" 36 | "log" 37 | 38 | "pass/util" 39 | ) 40 | 41 | type ( 42 | Application struct { 43 | Locked bool 44 | Icons map[string]bool 45 | } 46 | ) 47 | 48 | var ( 49 | win app.Contexter 50 | pass Application 51 | ) 52 | 53 | const ( 54 | MinimumPasswordLength = 8 55 | ) 56 | 57 | var config util.Configuration 58 | 59 | func GetImageName(filename string) string { 60 | if !pass.Icons[filename] { 61 | filename = "default" 62 | } 63 | return filename 64 | } 65 | 66 | func main() { 67 | // Pass is locked by default. 68 | pass.Locked = true 69 | 70 | // Load the config. 71 | config, _ = util.GetConfig(app.Resources()) 72 | 73 | log.Println(config) 74 | 75 | pass.Icons = util.ListAvailableIcons(app.Resources()) 76 | 77 | log.Println(pass.Icons) 78 | 79 | app.OnLaunch = func() { 80 | // Creates the AppMainMenu component. 81 | appMenu := &AppMainMenu{} 82 | 83 | // Mounts the AppMainMenu component into the application menu bar. 84 | if menuBar, ok := app.MenuBar(); ok { 85 | menuBar.Mount(appMenu) 86 | } 87 | 88 | // Create the main window 89 | win = newMainWindow() 90 | } 91 | 92 | app.OnReopen = func() { 93 | if win != nil { 94 | return 95 | } 96 | win = newMainWindow() 97 | } 98 | 99 | app.Run() 100 | } 101 | 102 | func newMainWindow() app.Contexter { 103 | // Creates a window context. 104 | win := app.NewWindow(app.Window{ 105 | Title: "Pass", 106 | Width: 300, 107 | Height: 548, 108 | Vibrancy: app.VibeDark, 109 | TitlebarHidden: true, 110 | OnClose: func() bool { 111 | win = nil 112 | return true 113 | }, 114 | }) 115 | 116 | log.Println("NewWindow") 117 | 118 | // Create component... 119 | ps := &UnlockScreen{} 120 | 121 | // ...and mount to window 122 | win.Mount(ps) 123 | 124 | // Return to context 125 | return win 126 | } 127 | -------------------------------------------------------------------------------- /menu.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Carl Löndahl. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Pass Desktop nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package main 32 | 33 | import ( 34 | "github.com/murlokswarm/app" 35 | _ "github.com/murlokswarm/log" 36 | ) 37 | 38 | type AppMainMenu struct { 39 | } 40 | 41 | func (m *AppMainMenu) Render() string { 42 | return ` 43 | 44 | 45 | 48 | 51 | 55 | 56 | 57 | 58 | ` 59 | } 60 | 61 | func (m *AppMainMenu) ShowSearchView() { 62 | if !pass.Locked { 63 | s := Search{} 64 | s.Prefetch("") 65 | win.Mount(&s) 66 | } 67 | } 68 | 69 | func (m *AppMainMenu) ShowAddView() { 70 | if !pass.Locked { 71 | s := Account{} 72 | win.Mount(&s) 73 | } 74 | } 75 | 76 | func (m *AppMainMenu) ShowAboutView() { 77 | s := About{} 78 | win.Mount(&s) 79 | } 80 | 81 | type EditMenu struct { 82 | } 83 | 84 | func (m *EditMenu) Render() string { 85 | return ` 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | ` 94 | } 95 | 96 | func init() { 97 | app.RegisterComponent(&AppMainMenu{}) 98 | app.RegisterComponent(&EditMenu{}) 99 | } 100 | -------------------------------------------------------------------------------- /otp.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Carl Löndahl. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Pass Desktop nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package main 32 | 33 | import ( 34 | "github.com/murlokswarm/app" 35 | "net/url" 36 | "pass/otp" 37 | "pass/rest" 38 | "log" 39 | ) 40 | 41 | type OTP struct { 42 | Title string 43 | Query string 44 | OTP string 45 | Data rest.DecodedEntry 46 | } 47 | 48 | func (*OTP) Render() string { 49 | return ` 50 |
51 |
52 | 62 |
63 |
67 | 68 |

{{.Title}}

69 |
70 |

{{.OTP}}

71 |
72 |
73 |
74 |
78 |
79 |
80 |
` 81 | } 82 | 83 | func (h *OTP) OnHref(URL *url.URL) { 84 | // Extract information from query and get account name and 85 | // its encrypted counterpart from query (this is need since 86 | // if we were to encrypt again, we would get a different 87 | // encrypted name). 88 | u := URL.Query() 89 | h.Title = u.Get("Name") 90 | restResponse, err := restClient.VaultReadSecret( 91 | &rest.Name{ 92 | Text: h.Title, 93 | Encrypted: u.Get("Encrypted"), 94 | }) 95 | 96 | if err != nil { 97 | log.Println(err) 98 | return 99 | } 100 | 101 | // Read contents from data segment. 102 | h.Data = *restResponse 103 | h.OTP = otp.ComputeOTPCode(h.Data.Password) 104 | 105 | app.Render(h) 106 | 107 | } 108 | 109 | func (h *OTP) Cancel() { 110 | NavigateBack("") 111 | } 112 | 113 | func (h *OTP) Delete() { 114 | d := h.Data.Name 115 | if d != nil { 116 | restClient.VaultDeleteSecret(&h.Data) 117 | } 118 | h.Cancel() 119 | } 120 | 121 | func (h *OTP) RefreshOTP(arg app.ChangeArg) { 122 | // Compute OTP based on retrieved data segment. 123 | h.OTP = otp.ComputeOTPCode(h.Data.Password) 124 | app.Render(h) 125 | } 126 | 127 | func (h *OTP) DoSearchQuery(arg app.ChangeArg) { 128 | NavigateBack(arg.Value) 129 | } 130 | 131 | func init() { 132 | app.RegisterComponent(&OTP{}) 133 | } 134 | -------------------------------------------------------------------------------- /otp/otp.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Carl Löndahl. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Pass Desktop nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package otp 32 | 33 | import ( 34 | "crypto/hmac" 35 | "crypto/sha1" 36 | "encoding/base32" 37 | "encoding/binary" 38 | "fmt" 39 | "time" 40 | ) 41 | 42 | func ComputeOTPCode(secret string) string { 43 | 44 | key, err := base32.StdEncoding.DecodeString(secret) 45 | if err != nil { 46 | return "n/a" 47 | } 48 | 49 | hash := hmac.New(sha1.New, key) 50 | err = binary.Write(hash, binary.BigEndian, 51 | int64(time.Now().Unix()/30)) 52 | 53 | if err != nil { 54 | return "n/a" 55 | } 56 | 57 | h := hash.Sum(nil) 58 | offset := h[19] & 0x0f 59 | truncated := binary.BigEndian.Uint32(h[offset:offset+4]) & 0x7fffffff 60 | return fmt.Sprintf("%06d", int(truncated%1000000)) 61 | } 62 | -------------------------------------------------------------------------------- /resources/buttons/arrow-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/buttons/arrow-down.png -------------------------------------------------------------------------------- /resources/buttons/arrow-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/buttons/arrow-up.png -------------------------------------------------------------------------------- /resources/buttons/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/buttons/check.png -------------------------------------------------------------------------------- /resources/buttons/dice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/buttons/dice.png -------------------------------------------------------------------------------- /resources/buttons/history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/buttons/history.png -------------------------------------------------------------------------------- /resources/buttons/signfile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/buttons/signfile.png -------------------------------------------------------------------------------- /resources/buttons/times.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/buttons/times.png -------------------------------------------------------------------------------- /resources/buttons/trash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/buttons/trash.png -------------------------------------------------------------------------------- /resources/css/pass.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: -apple-system; 3 | font-weight: 400; 4 | } 5 | 6 | body { 7 | background-size: cover; 8 | background-position: center; 9 | color: white; 10 | overflow: hidden; 11 | } 12 | 13 | ::-webkit-scrollbar { 14 | display: none; 15 | } 16 | 17 | h1 { 18 | font-weight: 400; 19 | font-size: 28px; 20 | text-align: center; 21 | margin-left: auto; 22 | margin-right: auto; 23 | } 24 | 25 | h2 { 26 | font-weight: 400; 27 | font-size: 18px; 28 | text-align: center; 29 | margin-left: auto; 30 | margin-right: auto; 31 | } 32 | 33 | h3 { 34 | font-size: 16px; 35 | font-weight: 400; 36 | margin-left: auto; 37 | margin-right: auto; 38 | } 39 | 40 | p { 41 | font-weight: 400; 42 | font-size: 14px; 43 | width: 90%; 44 | margin-left: 5%; 45 | max-width: 300; 46 | } 47 | 48 | a { 49 | color: white; 50 | cursor: default; 51 | } 52 | 53 | a:link { 54 | text-decoration: none; 55 | } 56 | 57 | span { 58 | display: inline-block; 59 | vertical-align: middle; 60 | } 61 | 62 | input { 63 | width: 80%; 64 | border: 0; 65 | outline: none; 66 | font-size: 16px; 67 | background: rgba(255, 255, 255, 0.2); 68 | color: white; 69 | -webkit-border-radius: 5px; 70 | -moz-border-radius: 5px; 71 | border-radius: 5px; 72 | pointer-events: auto; 73 | -webkit-user-select: auto; 74 | } 75 | 76 | ul { 77 | list-style-type: none; 78 | margin: 0; 79 | padding-left: 5%; 80 | width: 90%; 81 | } 82 | 83 | li img { 84 | float: left; 85 | list-style-type: none; 86 | margin: 0 12px 0 0; 87 | height: 8vh; 88 | max-height: 96px; 89 | border-radius: 7px; 90 | } 91 | 92 | li { 93 | list-style-type: none; 94 | padding: 4px; 95 | overflow: auto; 96 | border-radius: 5px; 97 | } 98 | 99 | li:hover { 100 | list-style-type: none; 101 | background: rgba(255, 255, 255, 0.1); 102 | } 103 | 104 | .WindowLayout { 105 | position: fixed; 106 | width: 100%; 107 | height: 100%; 108 | word-wrap: break-word; 109 | } 110 | 111 | .SearchLayout { 112 | padding-top: 20pt; 113 | -webkit-user-select: none; 114 | pointer-events: none; 115 | min-width: 200px; 116 | } 117 | 118 | .SearchListItemCaption { 119 | font-size: 16px; 120 | font-weight: 400; 121 | padding-top: -webkit-calc(3vh - 12px); 122 | max-height: 96px; 123 | } 124 | 125 | .SearchListItemLabel { 126 | font-size: 12px; 127 | font-weight: 300; 128 | } 129 | 130 | .PasswordEntryLayout { 131 | padding-top: 20pt; 132 | -webkit-user-select: none; 133 | pointer-events: none; 134 | } 135 | 136 | .grid-container { 137 | border-radius: 5px; 138 | display: inline-grid; 139 | grid-template-columns: auto auto auto auto; 140 | padding: 0px; 141 | } 142 | .grid-item { 143 | background-color: rgba(255, 255, 255, 0.8); 144 | border: 0px; 145 | padding: 16px; 146 | font-size: 30px; 147 | text-align: center; 148 | } 149 | 150 | ::-webkit-input-placeholder { 151 | color: #ccc; 152 | } 153 | 154 | @keyframes fadein { 155 | from { 156 | opacity:0; 157 | } 158 | to { 159 | opacity:1; 160 | } 161 | } 162 | 163 | @-webkit-keyframes fadein { 164 | from { 165 | opacity:0; 166 | } 167 | to { 168 | opacity:1; 169 | } 170 | } 171 | 172 | .animated { 173 | animation: fadein 1s; 174 | -moz-animation: fadein 1s; 175 | -webkit-animation: fadein 1s; 176 | -o-animation: fadein 1s; 177 | } 178 | 179 | .scrollable { 180 | overflow-y:scroll; 181 | max-height: -webkit-calc(100vh - 90px); 182 | margin-top: 10px; 183 | padding-top: -20px; 184 | pointer-events: auto; 185 | } 186 | 187 | .selectable { 188 | -webkit-user-select: auto; 189 | pointer-events: auto; 190 | } 191 | 192 | .clickable { 193 | pointer-events: auto; 194 | } 195 | 196 | .editable { 197 | background-color: rgba(255, 255, 255, 0.1);; 198 | color: #fafafa; 199 | display: block; 200 | margin : 0 auto; 201 | width: 80%; 202 | font-size: 16px; 203 | padding: 6px; 204 | border: 0; 205 | outline: none; 206 | -webkit-border-radius: 5px; 207 | -moz-border-radius: 5px; 208 | border-radius: 5px; 209 | pointer-events: auto; 210 | -webkit-user-select: auto; 211 | max-width: 300px 212 | } 213 | 214 | .editable:focus { 215 | color: white; 216 | background-color: rgba(255, 255, 255, 0.1); 217 | } 218 | 219 | .editable.searchfield { 220 | background: rgba(255, 255, 255, 0.1) url(../symbols/search_small.png) no-repeat scroll 8px 8px; 221 | background-size: 14px; 222 | padding-left: 30px; 223 | } 224 | 225 | .editable.password { 226 | background: rgba(255, 255, 255, 0.1) url(../symbols/key_small.png) no-repeat scroll 8px 8px; 227 | background-size: 14px; 228 | padding-left: 30px; 229 | } 230 | 231 | .editable.username { 232 | background: rgba(255, 255, 255, 0.1) url(../symbols/user_small.png) no-repeat scroll 8px 8px; 233 | background-size: 14px; 234 | padding-left: 30px; 235 | } 236 | 237 | .editable.server { 238 | background: rgba(255, 255, 255, 0.1) url(../symbols/server_small.png) no-repeat scroll 8px 8px; 239 | background-size: 14px; 240 | padding-left: 30px; 241 | } 242 | 243 | .editable.port { 244 | background: rgba(255, 255, 255, 0.1) url(../symbols/port_small.png) no-repeat scroll 8px 8px; 245 | background-size: 14px; 246 | padding-left: 30px; 247 | } 248 | 249 | .editable.name { 250 | background: rgba(255, 255, 255, 0); 251 | text-align: center; 252 | padding-top: 5px; 253 | padding-bottom: 5px; 254 | width: 100%; 255 | font-size: 28px; 256 | } 257 | 258 | .editable.ca { 259 | background: rgba(255, 255, 255, 0.1) url(../symbols/port_small.png) no-repeat scroll 10px 10px; 260 | background-size: 14px; 261 | padding-left: 30px; 262 | width: 60%; 263 | } 264 | 265 | .button { 266 | background: rgba(30, 34, 37, 0.0); 267 | transition: background 0.2s; 268 | background-size: 20px; 269 | background-repeat: no-repeat; 270 | background-position: center center; 271 | -webkit-border-radius: 5px; 272 | -moz-border-radius: 5px; 273 | border-radius: 5px; 274 | border: none; 275 | color: white; 276 | padding: 20px; 277 | width: 32px; 278 | height: 32px; 279 | pointer-events: auto; 280 | -webkit-user-select: auto; 281 | } 282 | 283 | .button:hover { 284 | background: rgba(255, 255, 255, 0.1); 285 | background-size: 20px; 286 | background-repeat: no-repeat; 287 | background-position: center center; 288 | transition: background 0.2s; 289 | pointer-events: auto; 290 | -webkit-user-select: auto; 291 | } 292 | 293 | .button:active { 294 | background: rgba(255, 255, 255, 0.3); 295 | background-size: 20px; 296 | background-repeat: no-repeat; 297 | background-position: center center; 298 | border-radius: 5px; 299 | border: none; 300 | color: white; 301 | padding: 20px; 302 | width: 32px; 303 | height: 32px; 304 | pointer-events: auto; 305 | -webkit-user-select: auto; 306 | } 307 | 308 | .button.ok { 309 | background-image:url(../buttons/check.png); 310 | } 311 | 312 | .button.cancel { 313 | background-image:url(../buttons/times.png); 314 | } 315 | 316 | .button.rerand{ 317 | background-image:url(../buttons/dice.png); 318 | } 319 | 320 | .button.add{ 321 | background-image:url(../buttons/arrow-up.png); 322 | } 323 | 324 | .button.download{ 325 | background-image:url(../buttons/arrow-down.png); 326 | } 327 | 328 | .button.refresh{ 329 | background-image:url(../buttons/history.png); 330 | } 331 | 332 | .button.delete { 333 | background-image:url(../buttons/trash.png); 334 | } 335 | 336 | .button.sign { 337 | background-image:url(../buttons/signfile.png); 338 | } 339 | 340 | .symbol { 341 | max-width: 100px; 342 | max-height: 100px; 343 | margin: auto; 344 | margin-top: -webkit-calc(20vh + 30px); 345 | } 346 | 347 | .symbol.trash { 348 | content:url(../symbols/trash.png); 349 | } 350 | 351 | .symbol.lock { 352 | content:url(../symbols/lock.png); 353 | } 354 | 355 | .symbol.search { 356 | content:url(../symbols/search.png); 357 | } 358 | 359 | .bottom-toolbar { 360 | height: 48px; 361 | width: 100%; 362 | text-align: center; 363 | opacity: 0.2; 364 | transition: opacity 0.5s; 365 | position: absolute; 366 | bottom: 0; 367 | } 368 | 369 | .bottom-toolbar:hover { 370 | height: 48px; 371 | width: 100%; 372 | opacity: 1; 373 | transition: opacity 0.5s; 374 | position: absolute; 375 | bottom: 0; 376 | } 377 | 378 | .trash-toolbar { 379 | height: 48px; 380 | text-align: center; 381 | position: absolute; 382 | bottom: 0; 383 | } 384 | -------------------------------------------------------------------------------- /resources/css/pass.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "mappings": "AAAA,IAAK;EACD,gBAAgB,EAAE,iBAAiB;EACnC,eAAe,EAAE,KAAK;EACtB,mBAAmB,EAAE,MAAM;EAC3B,KAAK,EAAE,KAAK;EACZ,QAAQ,EAAE,MAAM;;AAGpB,aAAc;EACV,QAAQ,EAAE,KAAK;EACf,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;EACZ,OAAO,EAAE,IAAI;EACb,eAAe,EAAE,MAAM;EACvB,WAAW,EAAE,MAAM;;AAGvB,SAAU;EACN,OAAO,EAAE,IAAI;;AAGjB,EAAG;EACC,WAAW,EAAE,GAAG;;AAGpB,KAAM;EACF,KAAK,EAAE,IAAI;EACX,OAAO,EAAE,GAAG;EACZ,MAAM,EAAE,CAAC;EACT,WAAW,EAAE,gBAAgB;EAC7B,OAAO,EAAE,IAAI;EACb,SAAS,EAAE,IAAI;EACf,UAAU,EAAE,WAAW;EACvB,KAAK,EAAE,KAAK;;AAGhB,WAAY;EACR,iBAAiB,EAAE,WAAW", 4 | "sources": ["../scss/pass.scss"], 5 | "names": [], 6 | "file": "pass.css" 7 | } -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/icon.png -------------------------------------------------------------------------------- /resources/iconpack/addthis500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/addthis500.png -------------------------------------------------------------------------------- /resources/iconpack/app-net.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/app-net.png -------------------------------------------------------------------------------- /resources/iconpack/behance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/behance.png -------------------------------------------------------------------------------- /resources/iconpack/blogger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/blogger.png -------------------------------------------------------------------------------- /resources/iconpack/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/default.png -------------------------------------------------------------------------------- /resources/iconpack/deviantart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/deviantart.png -------------------------------------------------------------------------------- /resources/iconpack/digg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/digg.png -------------------------------------------------------------------------------- /resources/iconpack/dribbble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/dribbble.png -------------------------------------------------------------------------------- /resources/iconpack/etsy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/etsy.png -------------------------------------------------------------------------------- /resources/iconpack/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/facebook.png -------------------------------------------------------------------------------- /resources/iconpack/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/file.png -------------------------------------------------------------------------------- /resources/iconpack/flickr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/flickr.png -------------------------------------------------------------------------------- /resources/iconpack/foursquare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/foursquare.png -------------------------------------------------------------------------------- /resources/iconpack/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/github.png -------------------------------------------------------------------------------- /resources/iconpack/gmail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/gmail.png -------------------------------------------------------------------------------- /resources/iconpack/googleplus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/googleplus.png -------------------------------------------------------------------------------- /resources/iconpack/grocid.net.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/grocid.net.png -------------------------------------------------------------------------------- /resources/iconpack/hangouts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/hangouts.png -------------------------------------------------------------------------------- /resources/iconpack/indiegogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/indiegogo.png -------------------------------------------------------------------------------- /resources/iconpack/instagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/instagram.png -------------------------------------------------------------------------------- /resources/iconpack/kickstarter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/kickstarter.png -------------------------------------------------------------------------------- /resources/iconpack/lastfm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/lastfm.png -------------------------------------------------------------------------------- /resources/iconpack/linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/linkedin.png -------------------------------------------------------------------------------- /resources/iconpack/livejournal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/livejournal.png -------------------------------------------------------------------------------- /resources/iconpack/meetup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/meetup.png -------------------------------------------------------------------------------- /resources/iconpack/messenger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/messenger.png -------------------------------------------------------------------------------- /resources/iconpack/mixcloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/mixcloud.png -------------------------------------------------------------------------------- /resources/iconpack/myspace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/myspace.png -------------------------------------------------------------------------------- /resources/iconpack/orkut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/orkut.png -------------------------------------------------------------------------------- /resources/iconpack/otp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/otp.png -------------------------------------------------------------------------------- /resources/iconpack/paypal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/paypal.png -------------------------------------------------------------------------------- /resources/iconpack/picasa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/picasa.png -------------------------------------------------------------------------------- /resources/iconpack/pinterest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/pinterest.png -------------------------------------------------------------------------------- /resources/iconpack/reddit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/reddit.png -------------------------------------------------------------------------------- /resources/iconpack/rss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/rss.png -------------------------------------------------------------------------------- /resources/iconpack/sharethis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/sharethis.png -------------------------------------------------------------------------------- /resources/iconpack/sign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/sign.png -------------------------------------------------------------------------------- /resources/iconpack/skype.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/skype.png -------------------------------------------------------------------------------- /resources/iconpack/soundcloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/soundcloud.png -------------------------------------------------------------------------------- /resources/iconpack/spotify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/spotify.png -------------------------------------------------------------------------------- /resources/iconpack/stackoverflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/stackoverflow.png -------------------------------------------------------------------------------- /resources/iconpack/steam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/steam.png -------------------------------------------------------------------------------- /resources/iconpack/stumbleupon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/stumbleupon.png -------------------------------------------------------------------------------- /resources/iconpack/tumblr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/tumblr.png -------------------------------------------------------------------------------- /resources/iconpack/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/twitter.png -------------------------------------------------------------------------------- /resources/iconpack/vimeo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/vimeo.png -------------------------------------------------------------------------------- /resources/iconpack/vk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/vk.png -------------------------------------------------------------------------------- /resources/iconpack/yelp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/yelp.png -------------------------------------------------------------------------------- /resources/iconpack/youtube-variation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/youtube-variation.png -------------------------------------------------------------------------------- /resources/iconpack/youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/iconpack/youtube.png -------------------------------------------------------------------------------- /resources/symbols/key_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/symbols/key_small.png -------------------------------------------------------------------------------- /resources/symbols/key_small_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/symbols/key_small_dark.png -------------------------------------------------------------------------------- /resources/symbols/lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/symbols/lock.png -------------------------------------------------------------------------------- /resources/symbols/lock_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/symbols/lock_dark.png -------------------------------------------------------------------------------- /resources/symbols/port_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/symbols/port_small.png -------------------------------------------------------------------------------- /resources/symbols/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/symbols/search.png -------------------------------------------------------------------------------- /resources/symbols/search_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/symbols/search_small.png -------------------------------------------------------------------------------- /resources/symbols/search_small_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/symbols/search_small_dark.png -------------------------------------------------------------------------------- /resources/symbols/server_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/symbols/server_small.png -------------------------------------------------------------------------------- /resources/symbols/trash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/symbols/trash.png -------------------------------------------------------------------------------- /resources/symbols/trash_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/symbols/trash_dark.png -------------------------------------------------------------------------------- /resources/symbols/user_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/symbols/user_small.png -------------------------------------------------------------------------------- /resources/symbols/user_small_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grocid/passdesktop/3cf1a940d1433002318fdd207c63acb66450d77e/resources/symbols/user_small_dark.png -------------------------------------------------------------------------------- /rest/encoding.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import "strings" 4 | 5 | const ( 6 | UserCredentialsSuffix = ",0" 7 | OTPSuffix = ",1" 8 | FileSuffix = ",2" 9 | SignSuffix = ",3" 10 | AccountLabel = "Account" 11 | OTPLabel = "OTP" 12 | FileLabel = "File" 13 | SignLabel = "Sign" 14 | ) 15 | 16 | func DecodeName(name string) (string, string, string) { 17 | 18 | if strings.HasSuffix(name, FileSuffix) { 19 | return name[:len(name)-2], FileLabel, "file" 20 | } 21 | 22 | if strings.HasSuffix(name, OTPSuffix) { 23 | return name[:len(name)-2], OTPLabel, "otp" 24 | } 25 | 26 | if strings.HasSuffix(name, SignSuffix) { 27 | return name[:len(name)-2], SignLabel, "sign" 28 | } 29 | 30 | return name, AccountLabel, name 31 | } 32 | -------------------------------------------------------------------------------- /rest/rest.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Carl Löndahl. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Pass Desktop nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package rest 32 | 33 | import ( 34 | "bytes" 35 | "crypto/tls" 36 | "crypto/x509" 37 | "encoding/json" 38 | "fmt" 39 | "io/ioutil" 40 | "log" 41 | "net/http" 42 | "pass/lock" 43 | "time" 44 | "errors" 45 | ) 46 | 47 | type ( 48 | UserData struct { 49 | Password string `json:"password"` 50 | Username string `json:"username"` 51 | File []byte `json:"file"` 52 | Padding string `json:"padding"` 53 | } 54 | 55 | MyRequestTag struct { 56 | Tag string `json:"tag"` 57 | } 58 | 59 | MyRequestEncrypted struct { 60 | Encrypted []byte `json:"encrypted"` 61 | } 62 | 63 | MyResponse struct { 64 | Data struct { 65 | Tag string `json:"tag"` 66 | Encrypted []byte `json:"encrypted"` 67 | Keys []string `json:"keys"` 68 | } `json:"data"` 69 | Errors []string `json:"errors"` 70 | } 71 | 72 | Name struct { 73 | Text string 74 | Encrypted string 75 | } 76 | 77 | DecodedEntry struct { 78 | Name *Name 79 | Username string 80 | Password string 81 | File []byte 82 | } 83 | ) 84 | 85 | const ( 86 | VaultTokenHeader = "X-Vault-Token" 87 | TagPath = "/updated" 88 | MinimumDataLength = 3 * 32 89 | 90 | MethodList = "LIST" 91 | ) 92 | 93 | type Client struct { 94 | LocalUpdate bool 95 | CachedTag string 96 | SearchResult []Name 97 | 98 | DecryptedToken string 99 | EncryptionKey []byte 100 | Lock *lock.Lock 101 | 102 | Client *http.Client 103 | EntryPoint string 104 | } 105 | 106 | func New(lock *lock.Lock) Client { 107 | 108 | r := Client{ 109 | LocalUpdate: true, 110 | Client: nil, 111 | Lock: lock, 112 | DecryptedToken: "", 113 | } 114 | 115 | return r 116 | } 117 | 118 | func (r *Client) Unlock(encryptedToken string) error { 119 | t, err := r.Lock.UnlockToken(encryptedToken) 120 | 121 | if err != nil { 122 | return err 123 | } 124 | 125 | r.DecryptedToken = t 126 | log.Println(t) 127 | 128 | return nil 129 | } 130 | 131 | func (r *Client) Init(hostname string, port int, CA string) { 132 | // Setup entrypoint 133 | r.EntryPoint = fmt.Sprintf("https://%s:%v/v1/secret", hostname, port) 134 | r.CachedTag = "-" 135 | 136 | // Create a TLS context... 137 | caCertPool := x509.NewCertPool() 138 | caCertPool.AppendCertsFromPEM([]byte(CA)) 139 | 140 | // ...and a client 141 | r.Client = &http.Client{ 142 | Transport: &http.Transport{ 143 | TLSClientConfig: &tls.Config{ 144 | RootCAs: caCertPool, 145 | }, 146 | }, 147 | Timeout: time.Second * 10, 148 | } 149 | } 150 | 151 | func (r *Client) EncHex(data string) (string, error) { 152 | encData, err := r.Lock.EncryptAndEncodeHex(data) 153 | return encData, err 154 | } 155 | 156 | func (r *Client) DecHex(data string) (string, error) { 157 | encData, err := r.Lock.HexDecodeAndDecrypt(data) 158 | return encData, err 159 | } 160 | 161 | func (r *Client) EncBase64(data string) ([]byte, error) { 162 | encData, err := r.Lock.EncryptAndEncodeBase64(data) 163 | return encData, err 164 | } 165 | 166 | func (r *Client) DecBase64(data []byte) (string, error) { 167 | encData, err := r.Lock.Base64DecodeAndDecrypt(data) 168 | return encData, err 169 | } 170 | 171 | func (r *Client) Request(operation string, s string, data *bytes.Buffer) (MyResponse, error) { 172 | var err error 173 | var req *http.Request 174 | 175 | // These two cases need to be handled separarely, i.e., the buffer must 176 | // explicitly be set to nil, we cannot pass a pointer with nil, or 177 | // program will throw a SIGSEGV. 178 | if data != nil { 179 | req, err = http.NewRequest(operation, r.EntryPoint+s, data) 180 | } else { 181 | req, err = http.NewRequest(operation, r.EntryPoint+s, nil) 182 | } 183 | 184 | if err != nil { 185 | return MyResponse{}, err 186 | } 187 | 188 | // Add header and do a GET for the specified entry... 189 | req.Header.Add(VaultTokenHeader, r.DecryptedToken) 190 | resp, err := r.Client.Do(req) 191 | 192 | // This should not happen, unless entry was deleted in the meantime... 193 | if err != nil { 194 | return MyResponse{}, err 195 | } 196 | 197 | // Read the body... 198 | defer resp.Body.Close() 199 | body, err := ioutil.ReadAll(resp.Body) 200 | 201 | response := MyResponse{} 202 | json.Unmarshal([]byte(body), &response) 203 | 204 | return response, nil 205 | } 206 | 207 | func (r *Client) UpdateTag() error { 208 | // Let the client know that we did an tag update and therefore 209 | // do not need to check it. 210 | r.LocalUpdate = true 211 | 212 | // Update the tag to indicate (for other clients) that something 213 | // has changed, i.e., we have done a PUT or a DELETE. To do so, 214 | // we generate random string, which w.h.p does not collide with 215 | // the existing one. 216 | storedTag := MyRequestTag{ 217 | Tag: string(lock.Entropy(32)), 218 | } 219 | 220 | // Convert to the payload to JSON. 221 | jsonStoredTag, err := json.Marshal(storedTag) 222 | 223 | // Put the new tag in place by doing a PUT on the tag path. 224 | _, err = r.Request(http.MethodPut, TagPath, 225 | bytes.NewBuffer([]byte(jsonStoredTag))) 226 | 227 | return err 228 | } 229 | 230 | func (r *Client) IsTagUpdated() bool { 231 | // If we did a PUT or DELETE from this client, we already know 232 | // it must be updated. 233 | if r.LocalUpdate { 234 | return true 235 | } 236 | 237 | // Obtain the value of the tag. 238 | vaultResponse, err := r.Request(http.MethodGet, TagPath, nil) 239 | 240 | if err != nil { 241 | return false 242 | } 243 | 244 | // Check whether old tag and obtained tag match or not. 245 | tagUpdated := r.CachedTag != vaultResponse.Data.Tag 246 | 247 | // Next time we call it, it will not differ if there was no 248 | // additional change between the calls. 249 | r.CachedTag = vaultResponse.Data.Tag 250 | 251 | return tagUpdated 252 | } 253 | 254 | func (r *Client) VaultReadSecret(data *Name) (*DecodedEntry, error) { 255 | log.Println("READ") 256 | 257 | // Retrieve data for a specific account. 258 | vaultResponse, err := r.Request(http.MethodGet, "/"+(*data).Encrypted, nil) 259 | 260 | if err != nil { 261 | return nil, err 262 | } 263 | 264 | // Decrypt 265 | decryptedData, err := r.DecBase64(vaultResponse.Data.Encrypted) 266 | 267 | // ...generate a DecodedEntry struct... 268 | decodedEntry := DecodedEntry{} 269 | err = json.Unmarshal([]byte(decryptedData), &decodedEntry) 270 | 271 | if err != nil { 272 | return nil, err 273 | } 274 | 275 | // ...with the proper information... 276 | decodedEntry.Name = data 277 | 278 | // ...and return to caller. 279 | return &decodedEntry, nil 280 | } 281 | 282 | func (r *Client) VaultWriteSecret(data *DecodedEntry) error { 283 | var padding string 284 | 285 | log.Println("WRITE", data) 286 | 287 | // Get some padding data, so that, ciphertext length 288 | // does not reveal information about password lenth. 289 | contentLength := len(data.Username) + len(data.Username) + len(data.File) 290 | 291 | if contentLength < MinimumDataLength { 292 | padding = string(lock.Entropy(MinimumDataLength - contentLength)) 293 | } 294 | 295 | userData := &UserData{ 296 | Username: (*data).Username, 297 | Password: (*data).Password, 298 | File: (*data).File, 299 | Padding: padding, 300 | } 301 | 302 | // Encode data as JSON... 303 | jsonUserData, _ := json.Marshal(userData) 304 | 305 | // ...and encrypt. 306 | encryptedUserData, _ := r.EncBase64(string(jsonUserData)) 307 | 308 | if (*data).Name.Encrypted == "" { 309 | (*data).Name.Encrypted, _ = r.EncHex((*data).Name.Text) 310 | } 311 | 312 | vaultRequestEncrypted := MyRequestEncrypted{ 313 | Encrypted: encryptedUserData, 314 | } 315 | jsonVaultRequestEncrypted, _ := json.Marshal(vaultRequestEncrypted) 316 | 317 | // Create the actual request. 318 | _, err := r.Request(http.MethodPut, "/"+(*data).Name.Encrypted, 319 | bytes.NewBuffer(jsonVaultRequestEncrypted)) 320 | 321 | if err != nil { 322 | return err 323 | } 324 | 325 | log.Println("OK") 326 | 327 | return r.UpdateTag() 328 | } 329 | 330 | func (r *Client) VaultDeleteSecret(data *DecodedEntry) error { 331 | if (*data).Name.Encrypted == "" { 332 | log.Fatal("No encrypted data stored") 333 | } 334 | 335 | _, err := r.Request(http.MethodDelete, "/"+(*data).Name.Encrypted, nil) 336 | 337 | if err != nil { 338 | return err 339 | } 340 | 341 | return r.UpdateTag() 342 | } 343 | 344 | //VaultRenameSecret 345 | 346 | func (r *Client) VaultListSecrets() (*[]Name, error) { 347 | log.Println("LIST") 348 | 349 | if r.IsTagUpdated() { 350 | // Do a LIST to get all entries. 351 | vaultResponse, err := r.Request(MethodList, "", nil) 352 | 353 | if err != nil { 354 | return nil, err 355 | } 356 | 357 | if len(vaultResponse.Errors) > 0 { 358 | return nil, errors.New(vaultResponse.Errors[0]) 359 | } 360 | 361 | r.SearchResult = make([]Name, 0) 362 | 363 | for _, key := range vaultResponse.Data.Keys { 364 | decrypted, err := r.DecHex(key) 365 | 366 | if err == nil { 367 | r.SearchResult = append(r.SearchResult, 368 | Name{ 369 | Text: decrypted, 370 | Encrypted: key, 371 | }) 372 | } 373 | } 374 | 375 | } else { 376 | log.Println("No tag change: using cached results") 377 | } 378 | return &r.SearchResult, nil 379 | } 380 | -------------------------------------------------------------------------------- /rest/rest_test.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "pass/lock" 7 | "testing" 8 | ) 9 | 10 | const ( 11 | token = "" 12 | salt = "" 13 | password = "" 14 | CA = "" 15 | server = "" 16 | port = 8200 17 | ) 18 | 19 | func TestListVaultSecrets(t *testing.T) { 20 | s, _ := hex.DecodeString(salt) 21 | mylock := lock.New(password, s) 22 | r := New(&mylock) 23 | r.Unlock(token) 24 | r.Init(server, port, CA) 25 | m, _ := r.VaultListSecrets() 26 | fmt.Println(*m) 27 | } 28 | 29 | func TestListAndGetFirstSecret(t *testing.T) { 30 | s, _ := hex.DecodeString(salt) 31 | mylock := lock.New(password, s) 32 | r := New(&mylock) 33 | r.Unlock(token) 34 | r.Init(server, port, CA) 35 | m, _ := r.VaultListSecrets() 36 | if r.IsTagUpdated() { 37 | t.Errorf("Tag update not properly working...") 38 | } 39 | fmt.Println(r.VaultReadSecret(&(*m)[0])) 40 | } 41 | 42 | func TestListAndGetFirstSecretAndWriteItAgain(t *testing.T) { 43 | s, _ := hex.DecodeString(salt) 44 | mylock := lock.New(password, s) 45 | r := New(&mylock) 46 | r.Unlock(token) 47 | r.Init(server, port, CA) 48 | m, _ := r.VaultListSecrets() 49 | f := r.VaultReadSecret(&(*m)[0]) 50 | (*f).Username = "zzz" 51 | r.VaultWriteSecret(f) 52 | f = r.VaultReadSecret(&(*m)[0]) 53 | if (*f).Username != "zzz" { 54 | t.Errorf("Write error") 55 | } 56 | (*f).Username = "mmm" 57 | r.VaultWriteSecret(f) 58 | } 59 | 60 | func CreateEmptySecretAndDeleteIt(t *testing.T) { 61 | s, _ := hex.DecodeString(salt) 62 | mylock := lock.New(password, s) 63 | r := New(&mylock) 64 | r.Unlock(token) 65 | r.Init(server, port, CA) 66 | 67 | } 68 | -------------------------------------------------------------------------------- /search.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Carl Löndahl. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Pass Desktop nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package main 32 | 33 | import ( 34 | "fmt" 35 | "log" 36 | "github.com/murlokswarm/app" 37 | "pass/rest" 38 | "strings" 39 | ) 40 | 41 | type Search struct { 42 | Query string 43 | Result []rest.Name 44 | } 45 | 46 | func (h *Search) Render() string { 47 | // Ouput 48 | filteredNameList := ` 49 |
50 |
51 | 62 |
64 |
65 |
    ` 66 | 67 | // Since we need to concatenate the results to a string, it is 68 | // cheapest (both in terms of memory and computations) to perform 69 | // filtering at this stage, rather than earlier filtering of the list. 70 | for _, name := range h.Result { 71 | // Due to optimization, we have encoded some data in the name. 72 | // We extract this data. 73 | title, label, image := rest.DecodeName(name.Text) 74 | 75 | // Get the icon if it exists, otherwise, substitue. 76 | filename := GetImageName(image) 77 | 78 | // Match the query against the current item to decide if we 79 | // should display it or not. 80 | if h.Query == "" || strings.Contains(strings.ToLower(title), strings.ToLower(h.Query)) { 81 | // Append to output. 82 | filteredNameList = filteredNameList + fmt.Sprintf(` 83 | 84 |
  • 85 | 86 |
    %s
    87 |
    %s
    88 |
  • 89 |
    `, 90 | label, title, name.Encrypted, filename, title, label) 91 | } 92 | } 93 | 94 | // Just to make the code look a bit cleaner. 95 | filteredNameList = filteredNameList + ` 96 |
97 |
98 |
99 |
100 |
` 101 | 102 | return filteredNameList 103 | } 104 | 105 | func (h *Search) Prefetch(query string) { 106 | // Fetch from Vault. 107 | r, err := restClient.VaultListSecrets() 108 | 109 | // Update query field. 110 | h.Query = query 111 | 112 | if err != nil { 113 | log.Println(err) 114 | return 115 | } 116 | 117 | h.Result = *r 118 | } 119 | 120 | func (h *Search) DoSearchQuery(arg app.ChangeArg) { 121 | // ...and fetch accounts based on query. 122 | h.Prefetch(arg.Value) 123 | app.Render(h) 124 | } 125 | 126 | func NavigateBack(query string) { 127 | // This is used by other windows, to get less code repetition. 128 | s := Search{} 129 | s.Prefetch(query) 130 | win.Mount(&s) 131 | } 132 | 133 | func init() { 134 | app.RegisterComponent(&Search{}) 135 | } 136 | -------------------------------------------------------------------------------- /sign.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Carl Löndahl. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Pass Desktop nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package main 32 | 33 | import ( 34 | "encoding/base64" 35 | "encoding/json" 36 | "github.com/murlokswarm/app" 37 | "golang.org/x/crypto/ed25519" 38 | "io/ioutil" 39 | "log" 40 | "net/url" 41 | "pass/rest" 42 | ) 43 | 44 | type Sign struct { 45 | Title string 46 | PublicBase64 string 47 | Keys KeyPair 48 | Data rest.DecodedEntry 49 | } 50 | 51 | type KeyPair struct { 52 | Priv []byte `json:"priv"` 53 | Pub []byte `json:"pub"` 54 | } 55 | 56 | var keyPair KeyPair 57 | 58 | const ( 59 | SignatureFileExtension = ".signature" 60 | ) 61 | 62 | func (h *Sign) Render() string { 63 | var gen string 64 | 65 | if len(h.Data.File) == 0 { 66 | gen = `
106 |
107 | 108 | ` 109 | 110 | } 111 | 112 | func (h *Sign) OnHref(URL *url.URL) { 113 | // Extract information from query and get account name and 114 | // its encrypted counterpart from query (this is need since 115 | // if we were to encrypt again, we would get a different 116 | // encrypted name). 117 | u := URL.Query() 118 | h.Title = u.Get("Name") 119 | restResponse, err := restClient.VaultReadSecret( 120 | &rest.Name{ 121 | Text: h.Title, 122 | Encrypted: u.Get("Encrypted"), 123 | }) 124 | 125 | if err != nil { 126 | log.Println(err) 127 | return 128 | } 129 | 130 | h.Data = *restResponse 131 | json.Unmarshal(h.Data.File, &keyPair) 132 | h.PublicBase64 = base64.StdEncoding.EncodeToString(keyPair.Pub) 133 | 134 | // Tells the app to update the rendering of the component. 135 | app.Render(h) 136 | } 137 | 138 | func (h *Sign) OK() { 139 | // Make sure we do not save already saved information. 140 | // Now, we just need to go back. 141 | h.Cancel() 142 | } 143 | 144 | func (h *Sign) Generate() { 145 | // By passing nil as argument, we default to 146 | // crypto/rand, which is desirable. 147 | pub, priv, err := ed25519.GenerateKey(nil) 148 | 149 | if err != nil { 150 | log.Println(err) 151 | return 152 | } 153 | 154 | keyPair := KeyPair{ 155 | Priv: priv, 156 | Pub: pub, 157 | } 158 | 159 | // We will store the key pair as JSON in the 160 | // the space where we would store file data. 161 | jsonKeyPair, err := json.Marshal(&keyPair) 162 | h.Data.File = jsonKeyPair 163 | 164 | // Write it to remote. 165 | err = restClient.VaultWriteSecret(&h.Data) 166 | 167 | if err != nil { 168 | log.Println(err) 169 | return 170 | } 171 | 172 | app.Render(h) 173 | } 174 | 175 | func (h *Sign) Cancel() { 176 | keyPair = KeyPair{} 177 | NavigateBack("") 178 | } 179 | 180 | func (h *Sign) Sign() { 181 | // Open filepicker window to get filename. 182 | app.NewFilePicker(app.FilePicker{ 183 | MultipleSelection: false, 184 | NoDir: true, 185 | NoFile: false, 186 | OnPick: func(filenames []string) { 187 | // Get contents of file. 188 | fileData, err := ioutil.ReadFile(filenames[0]) 189 | 190 | // If there was an error, probably due 191 | // to permissions, dump error message 192 | // to log. 193 | if err != nil { 194 | log.Println(err) 195 | return 196 | } 197 | 198 | // Generate signature from private key. 199 | signature := ed25519.Sign( 200 | ed25519.PrivateKey(keyPair.Priv), 201 | fileData) 202 | 203 | // Write to signature file, which is 204 | // the file we are signing + the 205 | // .signature file extension. 206 | err = ioutil.WriteFile( 207 | filenames[0]+SignatureFileExtension, 208 | signature, 0644) 209 | 210 | if err != nil { 211 | log.Println(err) 212 | return 213 | } 214 | 215 | app.Render(h) 216 | }, 217 | }) 218 | } 219 | 220 | func (h *Sign) DoSearchQuery(arg app.ChangeArg) { 221 | keyPair = KeyPair{} 222 | NavigateBack(arg.Value) 223 | } 224 | 225 | func (h *Sign) Delete() { 226 | keyPair = KeyPair{} 227 | d := h.Data.Name 228 | 229 | if d != nil { 230 | restClient.VaultDeleteSecret(&h.Data) 231 | } 232 | 233 | h.Cancel() 234 | } 235 | 236 | func init() { 237 | app.RegisterComponent(&Sign{}) 238 | } 239 | -------------------------------------------------------------------------------- /unlock.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Carl Löndahl. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Pass Desktop nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package main 32 | 33 | import ( 34 | "encoding/hex" 35 | "github.com/murlokswarm/app" 36 | "log" 37 | "pass/lock" 38 | "pass/rest" 39 | "pass/util" 40 | ) 41 | 42 | type UnlockScreen struct{} 43 | 44 | var restClient rest.Client 45 | 46 | func (h *UnlockScreen) OnDismount() { 47 | log.Println("UnlockScreen dismounted") 48 | } 49 | 50 | func (h *UnlockScreen) Render() string { 51 | return ` 52 |
53 |
54 |
55 | 65 |
66 |
67 |
68 |
` 69 | } 70 | 71 | func (h *UnlockScreen) Unlock(arg app.ChangeArg) { 72 | // Get the password from user input. 73 | password := arg.Value 74 | salt, _ := hex.DecodeString(config.Encrypted.Salt) 75 | 76 | // Verify password against encrypted token + mac. 77 | lock := lock.New(password, salt) 78 | _, err := lock.UnlockToken(config.Encrypted.Token) 79 | 80 | if err != nil { 81 | log.Println(err) 82 | return 83 | } 84 | 85 | log.Println("Unlocked.") 86 | 87 | // Signal to UI that the token was unlocked. 88 | pass.Locked = false 89 | 90 | // Setup the client for communication. 91 | restClient = rest.New(&lock) 92 | restClient.Init(config.Host, config.Port, config.CA) 93 | restClient.Unlock(config.Encrypted.Token) 94 | 95 | // Fetch the data from server. 96 | log.Println("Fetching data.") 97 | r, err := restClient.VaultListSecrets() 98 | 99 | if err != nil { 100 | log.Println(err) 101 | return 102 | } 103 | 104 | // Clear config to free up memory. 105 | config = util.Configuration{} 106 | 107 | // Mount search window. 108 | log.Println("OK", r) 109 | ps := &Search{Result: *r} 110 | win.Mount(ps) 111 | 112 | //app.Render(h) 113 | } 114 | 115 | func init() { 116 | // Register UI component 117 | app.RegisterComponent(&UnlockScreen{}) 118 | } 119 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2018 Carl Löndahl. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Pass Desktop nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package util 32 | 33 | import ( 34 | "encoding/json" 35 | "io/ioutil" 36 | "log" 37 | "os" 38 | "path/filepath" 39 | "strings" 40 | ) 41 | 42 | type Configuration struct { 43 | Encrypted struct { 44 | Token string `json:"token"` 45 | Salt string `json:"salt"` 46 | } `json:"encrypted"` 47 | 48 | Host string `json:"host"` 49 | Port int `json:"port"` 50 | CA string `json:"ca"` 51 | } 52 | 53 | const filename = "/config/config.json" 54 | const iconpath = "/iconpack/" 55 | 56 | func GetConfig(path string) (Configuration, error) { 57 | cfg := path + filename 58 | 59 | if _, err := os.Stat(cfg); os.IsNotExist(err) { 60 | log.Println("No config file present.") 61 | return Configuration{}, err 62 | } else { 63 | // Load config. 64 | return LoadConfiguration(cfg), nil 65 | } 66 | } 67 | 68 | func LoadConfiguration(file string) Configuration { 69 | var config Configuration 70 | 71 | // Try to open configuration file. 72 | configFile, err := os.Open(file) 73 | defer configFile.Close() 74 | 75 | // Bail out if there was an error reading it. 76 | if err != nil { 77 | log.Fatal(err.Error()) 78 | } 79 | 80 | // Decode the config. 81 | jsonParser := json.NewDecoder(configFile) 82 | jsonParser.Decode(&config) 83 | 84 | return config 85 | } 86 | 87 | func ListAvailableIcons(path string) map[string]bool { 88 | files, err := ioutil.ReadDir(path + iconpath) 89 | 90 | if err != nil { 91 | log.Fatal(err) 92 | } 93 | 94 | icons := map[string]bool{} 95 | 96 | for _, f := range files { 97 | icon := f.Name() 98 | icons[strings.TrimSuffix(icon, filepath.Ext(icon))] = true 99 | } 100 | 101 | return icons 102 | } 103 | --------------------------------------------------------------------------------