package org.cryptomator.cryptofs.common;

import com.google.common.io.BaseEncoding;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.AccessDeniedException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

/**
 * Utility class for generating a suffix for the backup file to make it unique to its original master key file.
 */
public final class MasterkeyBackupHelper {
	
	private static final Logger LOG = LoggerFactory.getLogger(MasterkeyBackupHelper.class);

	/**
	 * Computes the SHA-256 digest of the given byte array and returns a file suffix containing the first 4 bytes in hex string format.
	 *
	 * @param fileBytes the input byte for which the digest is computed
	 * @return "." + first 4 bytes of SHA-256 digest in hex string format
	 */
	public static String generateFileIdSuffix(byte[] fileBytes) {
		try {
			MessageDigest md = MessageDigest.getInstance("SHA-256");
			byte[] digest = md.digest(fileBytes);
			return "." + BaseEncoding.base16().encode(digest, 0, 4);
		} catch (NoSuchAlgorithmException e) {
			throw new IllegalStateException("Every Java Platform must support the Message Digest algorithm SHA-256", e);
		}
	}

	/**
	 * Do a best-effort attempt to backup the masterkey at the given path. Fail silently if a valid backup already exists.
	 * 
	 * @param masterKeyPath The masterkey file to backup
	 * @throws IOException Any non-recoverable I/O exception that occurs during this attempt
	 */
	public static Path backupMasterKey(Path masterKeyPath) throws IOException {
		byte[] keyFileContents = Files.readAllBytes(masterKeyPath);
		String backupFileName = masterKeyPath.getFileName().toString() + generateFileIdSuffix(keyFileContents) + Constants.MASTERKEY_BACKUP_SUFFIX;
		Path backupFilePath = masterKeyPath.resolveSibling(backupFileName);
		try (WritableByteChannel ch = Files.newByteChannel(backupFilePath, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
			ch.write(ByteBuffer.wrap(keyFileContents));
		} catch (AccessDeniedException e) {
			LOG.info("Storage device does not allow writing backup file. Comparing masterkey with backup directly.");
			assertExistingBackupMatchesContent(backupFilePath, ByteBuffer.wrap(keyFileContents));
		}
		return backupFilePath;
	}
	
	private static void assertExistingBackupMatchesContent(Path backupFilePath, ByteBuffer expectedContent) throws IOException {
		if (Files.exists(backupFilePath)) {
			// TODO replace by Files.mismatch() when using JDK > 12
			ByteBuffer buf = ByteBuffer.allocateDirect(expectedContent.remaining() + 1);
			try (ReadableByteChannel ch = Files.newByteChannel(backupFilePath, StandardOpenOption.READ)) {
				ch.read(buf);
				buf.flip();
				if (buf.compareTo(expectedContent) != 0) {
					throw new IllegalStateException("Corrupt masterkey backup: " + backupFilePath);
				}
				LOG.debug("Verified backup file: {}", backupFilePath);
			} catch (NoSuchFileException e) {
				LOG.warn("Did not find backup file: {}", backupFilePath);
			}
		}
	}
}

@@ -167,36 +167,8 @@
 on;%0A
-import java.io.InputStream;%0A
 impo
@@ -220,96 +220,12 @@
 els.
-FileChannel;%0Aimport java.nio.channels.ReadableByteChannel;%0Aimport java.nio.channels.Seek
+Read
 able
@@ -352,17 +352,38 @@
 ile.File
-s
+AlreadyExistsException
 ;%0Aimport
@@ -397,35 +397,21 @@
 io.file.
-NoSuchFileException
+Files
 ;%0Aimport
@@ -558,99 +558,8 @@
 ion;
-%0Aimport java.util.Arrays;%0A%0Aimport static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
 %0A%0A/*
@@ -2215,16 +2215,20 @@
 n.CREATE
+_NEW
 , Standa
@@ -2249,46 +2249,8 @@
 RITE
-, StandardOpenOption.TRUNCATE_EXISTING
 )) %7B
@@ -2334,121 +2334,41 @@
 ion 
-e) %7B%0A%09%09%09LOG.info(%22Storage device does not allow writing backup file. Comparing masterkey with backup directly.%22);
+%7C FileAlreadyExistsException e) %7B
 %0A%09%09%09
@@ -2453,16 +2453,99 @@
 ents));%0A
+%09%09%7D catch (IOException e) %7B%0A%09%09%09LOG.warn(%22Failed to backup valid masterkey file.%22);%0A
 %09%09%7D%0A%09%09re
@@ -2568,17 +2568,16 @@
 ath;%0A%09%7D%0A
-%09
 %0A%09privat
@@ -2673,35 +2673,16 @@
 Content)
- throws IOException
  %7B%0A%09%09if 
@@ -3041,70 +3041,125 @@
 %09%09%09%09
-throw new IllegalStateException(%22Corrupt masterkey backup: %22 +
+LOG.warn(%22Corrupt masterkey backup %7B%7D. Please replace it manually or unlock the vault on a writable storage device.%22,
  bac
@@ -3177,21 +3177,29 @@
 );%0A%09%09%09%09%7D
+ else %7B
 %0A%09%09%09%09
+%09
 LOG.debu
@@ -3241,24 +3241,30 @@
 pFilePath);%0A
+%09%09%09%09%7D%0A
 %09%09%09%7D catch (
@@ -3267,18 +3267,10 @@
 ch (
-NoSuchFile
+IO
 Exce
@@ -3298,53 +3298,63 @@
 rn(%22
-Did not find backup file: %7B%7D%22, backupFilePath
+Failed to compare valid masterkey with backup file.%22, e
 );%0A%09
@@ -3364,10 +3364,11 @@
 %0A%09%09%7D%0A%09%7D%0A
+%0A
 %7D%0A

change Masterkey backup method: * create backup if it does no exist * otherwise compare already existing backup file with masterkey * on io error or corrupt backup, _only_ log it (except reading masterkey file, then throw)