001/* 002 * Syncany, www.syncany.org 003 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com> 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU General Public License as published by 007 * the Free Software Foundation, either version 3 of the License, or 008 * (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU General Public License for more details. 014 * 015 * You should have received a copy of the GNU General Public License 016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 017 */ 018package org.syncany.operations.init; 019 020import java.io.File; 021import java.io.FileInputStream; 022import java.util.logging.Level; 023import java.util.logging.Logger; 024 025import org.apache.commons.io.FileUtils; 026import org.simpleframework.xml.Serializer; 027import org.simpleframework.xml.core.Persister; 028import org.syncany.config.Config; 029import org.syncany.config.DaemonConfigHelper; 030import org.syncany.config.to.ConfigTO; 031import org.syncany.config.to.MasterTO; 032import org.syncany.config.to.RepoTO; 033import org.syncany.crypto.CipherException; 034import org.syncany.crypto.CipherUtil; 035import org.syncany.crypto.SaltedSecretKey; 036import org.syncany.operations.daemon.messages.ShowMessageExternalEvent; 037import org.syncany.operations.init.ConnectOperationOptions.ConnectOptionsStrategy; 038import org.syncany.operations.init.ConnectOperationResult.ConnectResultCode; 039import org.syncany.plugins.UserInteractionListener; 040import org.syncany.plugins.transfer.StorageException; 041import org.syncany.plugins.transfer.StorageTestResult; 042import org.syncany.plugins.transfer.TransferManager; 043import org.syncany.plugins.transfer.TransferSettings; 044import org.syncany.plugins.transfer.files.MasterRemoteFile; 045import org.syncany.plugins.transfer.files.RemoteFile; 046import org.syncany.plugins.transfer.files.SyncanyRemoteFile; 047 048/** 049 * The connect operation connects to an existing repository at a given remote storage 050 * location. Its responsibilities include: 051 * 052 * <ul> 053 * <li>Downloading of the repo file. If it is encrypted, also downloading the master 054 * file to allow decrypting the repo file.</li> 055 * <li>If encrypted: Querying the user for the password and creating the master key using 056 * the password and the master salt.</li> 057 * <li>If encrypted: Decrypting and verifying the repo file.</li> 058 * <li>Creating the local Syncany folder structure in the local directory (.syncany 059 * folder and the sub-structure) and copying the repo/master file to it.</li> 060 * </ul> 061 * 062 * @author Philipp C. Heckel (philipp.heckel@gmail.com) 063 */ 064public class ConnectOperation extends AbstractInitOperation { 065 private static final Logger logger = Logger.getLogger(ConnectOperation.class.getSimpleName()); 066 067 private static final int MAX_RETRY_PASSWORD_COUNT = 3; 068 private int retryPasswordCount = 0; 069 070 private final ConnectOperationOptions options; 071 private final ConnectOperationResult result; 072 073 private TransferManager transferManager; 074 075 public ConnectOperation(ConnectOperationOptions options, UserInteractionListener listener) { 076 super(null, listener); 077 078 this.options = options; 079 this.result = new ConnectOperationResult(); 080 } 081 082 @Override 083 public ConnectOperationResult execute() throws Exception { 084 logger.log(Level.INFO, ""); 085 logger.log(Level.INFO, "Running 'Connect'"); 086 logger.log(Level.INFO, "--------------------------------------------"); 087 088 // Decrypt and init configTO 089 ConfigTO configTO = null; 090 091 try { 092 configTO = createConfigTO(); 093 } 094 catch (CipherException e) { 095 logger.log(Level.FINE, "Could not create config", e); 096 return new ConnectOperationResult(ConnectResultCode.NOK_DECRYPT_ERROR); 097 } 098 099 // Init plugin and transfer manager 100 transferManager = createTransferManagerFromNullConfig(options.getConfigTO()); 101 102 // Test the repo 103 if (!performRepoTest(transferManager)) { 104 logger.log(Level.INFO, "- Connecting to the repo failed, repo already exists or cannot be created: " + result.getResultCode()); 105 return result; 106 } 107 108 logger.log(Level.INFO, "- Connecting to the repo was successful; now downloading repo file ..."); 109 110 // Create local .syncany directory 111 File tmpRepoFile = downloadFile(transferManager, new SyncanyRemoteFile()); 112 113 if (CipherUtil.isEncrypted(tmpRepoFile)) { 114 logger.log(Level.INFO, "- Repo is ENCRYPTED. Decryption necessary."); 115 116 if (configTO.getMasterKey() == null) { 117 logger.log(Level.INFO, "- No master key present; Asking for password ..."); 118 119 boolean retryPassword = true; 120 121 while (retryPassword) { 122 SaltedSecretKey possibleMasterKey = askPasswordAndCreateMasterKey(); 123 logger.log(Level.INFO, "- Master key created. Now verifying by decrypting repo file..."); 124 125 if (decryptAndVerifyRepoFile(tmpRepoFile, possibleMasterKey)) { 126 logger.log(Level.INFO, "- SUCCESS: Repo file decrypted successfully."); 127 128 configTO.setMasterKey(possibleMasterKey); 129 retryPassword = false; 130 } 131 else { 132 logger.log(Level.INFO, "- FAILURE: Repo file decryption failed. Asking for retry."); 133 retryPassword = askRetryPassword(); 134 135 if (!retryPassword) { 136 logger.log(Level.INFO, "- No retry possible/desired. Returning NOK_DECRYPT_ERROR."); 137 return new ConnectOperationResult(ConnectResultCode.NOK_DECRYPT_ERROR); 138 } 139 } 140 } 141 } 142 else { 143 logger.log(Level.INFO, "- Master key present; Now verifying by decrypting repo file..."); 144 145 if (!decryptAndVerifyRepoFile(tmpRepoFile, configTO.getMasterKey())) { 146 logger.log(Level.INFO, "- FAILURE: Repo file decryption failed. Returning NOK_DECRYPT_ERROR."); 147 return new ConnectOperationResult(ConnectResultCode.NOK_DECRYPT_ERROR); 148 } 149 } 150 } 151 else { 152 String repoFileStr = FileUtils.readFileToString(tmpRepoFile); 153 verifyRepoFile(repoFileStr); 154 } 155 156 // Success, now do the work! 157 File appDir = createAppDirs(options.getLocalDir()); 158 159 // Write file 'config.xml' 160 File configFile = new File(appDir, Config.FILE_CONFIG); 161 configTO.save(configFile); 162 163 // Write file 'syncany' 164 File repoFile = new File(appDir, Config.FILE_REPO); 165 FileUtils.copyFile(tmpRepoFile, repoFile); 166 tmpRepoFile.delete(); 167 168 // Write file 'master' 169 if (configTO.getMasterKey() != null) { 170 File masterFile = new File(appDir, Config.FILE_MASTER); 171 new MasterTO(configTO.getMasterKey().getSalt()).save(masterFile); 172 } 173 174 // Shutdown plugin 175 transferManager.disconnect(); 176 177 // Add to daemon (if requested) 178 if (options.isDaemon()) { 179 try { 180 boolean addedToDaemonConfig = DaemonConfigHelper.addFolder(options.getLocalDir()); 181 result.setAddedToDaemon(addedToDaemonConfig); 182 } 183 catch (Exception e) { 184 logger.log(Level.WARNING, "Cannot add folder to daemon config.", e); 185 result.setAddedToDaemon(false); 186 } 187 } 188 189 result.setResultCode(ConnectResultCode.OK); 190 return result; 191 } 192 193 private boolean decryptAndVerifyRepoFile(File tmpRepoFile, SaltedSecretKey masterKey) throws StorageException { 194 try { 195 String repoFileStr = decryptRepoFile(tmpRepoFile, masterKey); 196 verifyRepoFile(repoFileStr); 197 198 return true; 199 } 200 catch (CipherException e) { 201 logger.log(Level.FINE, "Could not decrypt the repository file", e); 202 return false; 203 } 204 } 205 206 private SaltedSecretKey askPasswordAndCreateMasterKey() throws CipherException, StorageException { 207 File tmpMasterFile = downloadFile(transferManager, new MasterRemoteFile()); 208 MasterTO masterTO = readMasterFile(tmpMasterFile); 209 210 tmpMasterFile.delete(); 211 212 String masterKeyPassword = getOrAskPassword(); 213 byte[] masterKeySalt = masterTO.getSalt(); 214 215 return createMasterKeyFromPassword(masterKeyPassword, masterKeySalt); // This takes looong! 216 } 217 218 private ConfigTO createConfigTO() throws StorageException, CipherException { 219 ConfigTO configTO = options.getConfigTO(); 220 221 if (options.getStrategy() == ConnectOptionsStrategy.CONNECTION_TO) { 222 return configTO; 223 } 224 else if (options.getStrategy() == ConnectOptionsStrategy.CONNECTION_LINK) { 225 return createConfigTOFromLink(configTO, options.getConnectLink(), options.getPassword()); 226 } 227 else { 228 throw new RuntimeException("Unhandled connect strategy: " + options.getStrategy()); 229 } 230 } 231 232 private ConfigTO createConfigTOFromLink(ConfigTO configTO, String link, String masterPassword) throws StorageException, CipherException { 233 logger.log(Level.INFO, "Creating config TO from link: " + link + " ..."); 234 ApplicationLink applicationLink = new ApplicationLink(link); 235 236 try { 237 if (applicationLink.isEncrypted()) { 238 // Non-interactive mode 239 if (masterPassword != null) { 240 logger.log(Level.INFO, " - Link is encrypted. Password available."); 241 242 SaltedSecretKey masterKey = createMasterKeyFromPassword(masterPassword, applicationLink.getMasterKeySalt()); 243 TransferSettings transferSettings = applicationLink.createTransferSettings(masterKey); 244 245 configTO.setMasterKey(masterKey); 246 configTO.setTransferSettings(transferSettings); 247 } 248 else { 249 logger.log(Level.INFO, " - Link is encrypted. Asking for password."); 250 251 boolean retryPassword = true; 252 253 while (retryPassword) { 254 // Ask password 255 masterPassword = getOrAskPassword(); 256 257 // Generate master key 258 SaltedSecretKey masterKey = createMasterKeyFromPassword(masterPassword, applicationLink.getMasterKeySalt()); 259 260 // Decrypt config 261 try { 262 TransferSettings transferSettings = applicationLink.createTransferSettings(masterKey); 263 264 configTO.setMasterKey(masterKey); 265 configTO.setTransferSettings(transferSettings); 266 267 retryPassword = false; 268 } 269 catch (CipherException e) { 270 retryPassword = askRetryPassword(); 271 } 272 } 273 } 274 275 if (configTO.getTransferSettings() == null) { 276 throw new CipherException("Unable to decrypt link."); 277 } 278 } 279 else { 280 logger.log(Level.INFO, " - Link is NOT encrypted. No password needed."); 281 282 TransferSettings transferSettings = applicationLink.createTransferSettings(); 283 configTO.setTransferSettings(transferSettings); 284 } 285 } 286 catch (Exception e) { 287 throw new StorageException("Unable to extract connection settings: " + e.getMessage(), e); 288 } 289 290 return configTO; 291 } 292 293 private boolean performRepoTest(TransferManager transferManager) { 294 StorageTestResult testResult = transferManager.test(false); 295 296 logger.log(Level.INFO, "Storage test result ist " + testResult); 297 298 if (testResult.isRepoFileExists()) { 299 logger.log(Level.INFO, "--> OKAY: Repo file exists. We're good to go!"); 300 return true; 301 } 302 else { 303 logger.log(Level.INFO, "--> NOT OKAY: Invalid target/repo state. Operation cannot be continued."); 304 305 result.setResultCode(ConnectResultCode.NOK_TEST_FAILED); 306 result.setTestResult(testResult); 307 308 return false; 309 } 310 } 311 312 private String getOrAskPassword() { 313 if (options.getPassword() == null) { 314 if (listener == null) { 315 throw new RuntimeException("Repository file is encrypted, but password cannot be queried (no listener)."); 316 } 317 318 return listener.onUserPassword(null, "Master Password: "); 319 } 320 else { 321 return options.getPassword(); 322 } 323 } 324 325 private boolean askRetryPassword() { 326 retryPasswordCount++; 327 328 if (retryPasswordCount < MAX_RETRY_PASSWORD_COUNT) { 329 int triesLeft = MAX_RETRY_PASSWORD_COUNT - retryPasswordCount; 330 String triesLeftStr = triesLeft != 1 ? triesLeft + " tries left." : "Last chance."; 331 332 eventBus.post(new ShowMessageExternalEvent("ERROR: Invalid password or corrupt ciphertext. " + triesLeftStr)); 333 return true; 334 } 335 else { 336 return false; 337 } 338 } 339 340 protected File downloadFile(TransferManager transferManager, RemoteFile remoteFile) throws StorageException { 341 try { 342 File tmpRepoFile = File.createTempFile("syncanyfile", "tmp"); 343 344 transferManager.download(remoteFile, tmpRepoFile); 345 return tmpRepoFile; 346 } 347 catch (Exception e) { 348 throw new StorageException("Unable to connect to repository.", e); 349 } 350 } 351 352 private SaltedSecretKey createMasterKeyFromPassword(String masterPassword, byte[] masterKeySalt) throws CipherException { 353 fireNotifyCreateMaster(); 354 355 SaltedSecretKey masterKey = CipherUtil.createMasterKey(masterPassword, masterKeySalt); 356 return masterKey; 357 } 358 359 private String decryptRepoFile(File file, SaltedSecretKey masterKey) throws CipherException { 360 try { 361 logger.log(Level.INFO, "Decrypting repo file ..."); 362 363 FileInputStream encryptedRepoConfig = new FileInputStream(file); 364 String repoFileStr = new String(CipherUtil.decrypt(encryptedRepoConfig, masterKey)); 365 366 logger.log(Level.INFO, "Repo file decrypted:"); 367 logger.log(Level.INFO, repoFileStr); 368 369 return repoFileStr; 370 } 371 catch (Exception e) { 372 logger.log(Level.INFO, "Invalid password given, or repo file corrupt.", e); 373 throw new CipherException("Invalid password given, or repo file corrupt.", e); 374 } 375 } 376 377 private void verifyRepoFile(String repoFileStr) throws StorageException { 378 try { 379 Serializer serializer = new Persister(); 380 serializer.read(RepoTO.class, repoFileStr); 381 } 382 catch (Exception e) { 383 throw new StorageException("Repo file corrupt.", e); 384 } 385 } 386 387 private MasterTO readMasterFile(File tmpMasterFile) throws StorageException { 388 try { 389 Serializer serializer = new Persister(); 390 return serializer.read(MasterTO.class, tmpMasterFile); 391 } 392 catch (Exception e) { 393 throw new StorageException("Master file corrupt.", e); 394 } 395 } 396}