· 7 years ago · Jun 12, 2018, 08:10 AM
1/*******************************************************************************
2 * Copyright 2018 Ortis (cao.ortis.org@gmail.com)
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
5 *
6 * http://www.apache.org/licenses/LICENSE-2.0
7 *
8 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
9 * License for the specific language governing permissions and limitations under the License.
10 ******************************************************************************/
11
12package org.ortis.jsafebox;
13
14import java.io.ByteArrayInputStream;
15import java.io.ByteArrayOutputStream;
16import java.io.Closeable;
17import java.io.File;
18import java.io.IOException;
19import java.io.InputStream;
20import java.io.OutputStream;
21import java.io.RandomAccessFile;
22import java.lang.reflect.Type;
23import java.nio.ByteBuffer;
24import java.nio.charset.StandardCharsets;
25import java.nio.file.Files;
26import java.security.Key;
27import java.security.SecureRandom;
28import java.util.Arrays;
29import java.util.Collections;
30import java.util.HashMap;
31import java.util.LinkedHashMap;
32import java.util.Map;
33import java.util.TreeMap;
34import java.util.concurrent.CancellationException;
35
36import javax.crypto.Cipher;
37import javax.crypto.SecretKey;
38import javax.crypto.spec.IvParameterSpec;
39import javax.crypto.spec.SecretKeySpec;
40
41import org.ortis.jsafebox.hash.Hasher;
42import org.ortis.jsafebox.hash.SHA256;
43import org.ortis.jsafebox.task.TaskProbe;
44
45import com.google.gson.Gson;
46import com.google.gson.reflect.TypeToken;
47
48/**
49 * Virtual vault where files are stored
50 *
51 * @author Ortis <br>
52 * 2018 Apr 26 7:29:29 PM <br>
53 */
54public class Safe implements Closeable
55{
56 public static final String VERSION = "0.2 beta";
57
58 public static final Gson GSON = new Gson();
59 private static final Type MAP_STRING_STRING_TYPE = new TypeToken<Map<String, String>>()
60 {
61 }.getType();
62
63 public static final Type BYTE_ARRAY_TYPE = new TypeToken<byte []>()
64 {
65 }.getType();
66
67 private final static Hasher HASHER = new SHA256();
68
69 public static final String ENCRYPTION_LABEL = "encryption";
70 public final static String ENCRYPTION_IV_LENGTH_LABEL = "iv length";
71 public static final String KEY_ALGO_LABEL = "algo";
72 public static final String PROTOCOL_SPEC_LABEL = "protocol description";
73 public static final String PBKDF2_SALT_LABEL = "pbkdf2 salt";
74 public static final int PBKDF2_ITERATION = 100000;
75 public static final String PBKDF2_ITERATION_LABEL = "pbkdf2 iteration";
76
77 public static final String PROTOCOL_SPEC = "JSafebox is using a very simple protocol so encrypted files can be easily read by another program, as long as you have the password. The encryption key is derived from the password using PBKDF2 hashing with 100000 iteration. A JSafebox file contains a SHA256 integrity hash followed by blocks: [ integrity hash | block 0 | block 1 | ... | block N ]. Each block is stored as followed: [ IV | metadata length | metadata | data length | data ] where 'IV' is the Initialization_vector of the encryption (16 bytes), 'metadata' is a JSON string and 'length' are 64 bits (8 bytes) integer. The first block 'block 0' is the 'header' and is the only block not encrypted and therefore, the only block without IV. The 'header' only have metadata ('data length' is 0) and contains text entries specified by the user and various additional entries including a protocol explanation, the type of encoding and the parameters of the encryption. The 'header's metadata is stored as JSON string and can be seen by opening the safe file with a basic text editor. The second block 'block 1' is the 'properties'. It is similar to the 'header' except that it is encrypted and have an IV. The 'properties' contains text entries specified by the user and stored in JSON. The following blocks (from 2 to N) are the encrypted files. (Full manual at https://github.com/0rtis/jsafebox)";
78
79 private final File originalFile;
80
81 private final SecretKey encryptionKey;
82 private final int ivLength;
83 private final RandomAccessFile original;
84
85 private final File tempFile;
86 private final RandomAccessFile temp;
87
88 private final byte [] hash;
89
90 private final Map<String, String> publicHeader;
91 private final Map<String, String> privateProperties;
92 private final Map<String, Block> roBlocks;
93 private final Map<String, Block> blocks;
94 private final Map<String, Block> tempBlocks;
95 private final Map<String, Block> deletedBlocks;
96
97 private final int bufferSize;
98
99 private final Folder root;
100
101 /**
102 * Create an instance of {@link Safe}
103 *
104 * @param file:
105 * the safe file
106 * @param cipher:
107 * cipher to decrypt the data
108 * @param keySpec:
109 * key specification
110 * @param algoSpec:
111 * encryption specification
112 * @param bufferSize:
113 * size of the <code>byte</code> buffer to be used in IO operation
114 * @throws Exception
115 */
116 public Safe(final File file, final SecretKey key, final int bufferSize) throws Exception
117 {
118
119 this.originalFile = file.getAbsoluteFile();
120
121 this.encryptionKey = key;
122
123 this.bufferSize = bufferSize;
124
125 this.original = new RandomAccessFile(file, "rw");
126 this.tempFile = Files.createTempFile(null, null).toFile();
127 this.temp = new RandomAccessFile(this.tempFile, "rw");
128
129 final HashMap<String, String> publicProps = new LinkedHashMap<>();
130 this.publicHeader = Collections.unmodifiableMap(publicProps);
131 final HashMap<String, String> props = new LinkedHashMap<>();
132 this.privateProperties = Collections.unmodifiableMap(props);
133 this.blocks = new LinkedHashMap<>();
134 this.roBlocks = Collections.unmodifiableMap(blocks);
135 this.tempBlocks = new LinkedHashMap<>();
136 this.deletedBlocks = new LinkedHashMap<>();
137 this.root = new Folder(null, Folder.ROOT_NAME);
138
139 final byte [] buffer = new byte[bufferSize];
140 final byte [] outBuffer = new byte[bufferSize];
141
142 this.original.read(buffer, 0, HASHER.getHashLength());
143 this.hash = new byte[HASHER.getHashLength()];
144 System.arraycopy(buffer, 0, this.hash, 0, this.hash.length);
145
146 long length;
147 int read;
148 final ByteArrayOutputStream baos = new ByteArrayOutputStream(buffer.length);
149
150 final long headerLength = this.original.readLong();
151 length = headerLength;
152
153 while (length > 0)
154 {
155
156 if (length < buffer.length)
157 read = this.original.read(buffer, 0, (int) length);
158 else
159 read = this.original.read(buffer);
160
161 baos.write(buffer, 0, read);
162 length -= read;
163
164 }
165
166 String json = new String(baos.toByteArray(), StandardCharsets.UTF_8);
167
168 publicProps.putAll(GSON.fromJson(json, MAP_STRING_STRING_TYPE));
169
170 this.ivLength = Integer.parseInt(this.publicHeader.get(ENCRYPTION_IV_LENGTH_LABEL));
171
172 this.original.readLong();// data length 0
173
174 // init cipher
175 final Cipher cipher = getCipher();
176
177 // read private properties
178 this.original.read(buffer, 0, this.ivLength);// read properties iv
179 IvParameterSpec iv = new IvParameterSpec(Arrays.copyOf(buffer, this.ivLength));
180 cipher.init(Cipher.DECRYPT_MODE, this.encryptionKey, iv);
181
182 final long propLength = this.original.readLong();
183 length = propLength;
184
185 baos.reset();
186
187 while (length > 0)
188 {
189
190 if (length < buffer.length)
191 read = this.original.read(buffer, 0, (int) length);
192 else
193 read = this.original.read(buffer);
194
195 final int decrypted = cipher.update(buffer, 0, read, outBuffer);
196
197 baos.write(outBuffer, 0, decrypted);
198 length -= read;
199
200 }
201
202 baos.write(cipher.doFinal());
203
204 json = new String(baos.toByteArray());
205
206 props.putAll(GSON.fromJson(json, MAP_STRING_STRING_TYPE));
207 this.original.readLong();// data length 0
208
209 while (this.original.getFilePointer() < this.original.length())
210 {
211 baos.reset();
212
213 final long offset = this.original.getFilePointer();
214
215 this.original.read(buffer, 0, this.ivLength);// read properties iv
216 iv = new IvParameterSpec(Arrays.copyOf(buffer, this.ivLength));
217 cipher.init(Cipher.DECRYPT_MODE, this.encryptionKey, iv);
218
219 final long metaLength = this.original.readLong();
220 final long metaOffset = this.original.getFilePointer();
221
222 length = metaLength;
223 while (length > 0)
224 {
225
226 if (length < buffer.length)
227 read = this.original.read(buffer, 0, (int) length);
228 else
229 read = this.original.read(buffer);
230
231 final int decrypted = cipher.update(buffer, 0, read, outBuffer);
232 baos.write(outBuffer, 0, decrypted);
233 length -= read;
234 }
235 baos.write(cipher.doFinal());
236 json = new String(baos.toByteArray());
237
238 final Map<String, String> properties = new HashMap<>(GSON.fromJson(json, MAP_STRING_STRING_TYPE));
239 final String path = properties.get(Block.PATH_LABEL);
240 if (path == null)
241 throw new IllegalStateException("Path of block starting at " + offset + " is not set");
242
243 if (blocks.containsKey(path.toUpperCase(Environment.getLocale())))
244 throw new IllegalStateException("Block path " + path + " already exist");
245
246 final long dataLength = this.original.readLong();
247 final long dataOffset = this.original.getFilePointer();
248
249 final String [] tokens = path.split(Folder.REGEX_DELIMITER);
250
251 this.root.mkdir(tokens, 1, true);
252
253 final org.ortis.jsafebox.SafeFile dstFile;
254
255 if (tokens.length == 2)
256 dstFile = this.root;
257 else
258 dstFile = this.root.get(tokens, 1, tokens.length - 1);
259
260 if (dstFile == null)
261 throw new Exception("Could not find destination folder for block path " + path);
262
263 if (!dstFile.isFolder())
264 throw new Exception("Destination folder " + dstFile + " is a block");
265
266 final Folder destinationFolder = ((Folder) dstFile);
267
268 final long blockLength = original.getFilePointer() - offset + dataLength;
269 final Block block = new Block(path, properties, offset, blockLength, metaOffset, metaLength, dataOffset, dataLength, destinationFolder);
270
271 destinationFolder.add(block);
272
273 blocks.put(block.getComparablePath(), block);
274 this.original.seek(block.getOffset() + block.getLength());
275 }
276
277 }
278
279 /**
280 * Add data into the {@link Safe}. <b>Note that the data will be stored into the temporary safe file</b>. Use {@link Safe#save()} to save all temporary data
281 *
282 * @param properties:
283 * metadata
284 * @param data:
285 * data to encrypt
286 * @return
287 * @throws Exception
288 */
289 public synchronized Block add(final Map<String, String> properties, final InputStream data, TaskProbe probe) throws Exception
290 {
291 if (probe == null)
292 probe = TaskProbe.DULL_PROBE;
293
294 try
295 {
296 final String path = properties.get(Block.PATH_LABEL);
297
298 if (path == null)
299 throw new IllegalArgumentException("Property " + Block.PATH_LABEL + " is missing");
300
301 org.ortis.jsafebox.SafeFile destinationFile = SafeFiles.get(path, this.root, this.root);
302
303 if (destinationFile != null)
304 throw new Exception("Block file " + destinationFile + " already exist");
305
306 final String comparablePath = properties.get(Block.PATH_LABEL).toUpperCase(Environment.getLocale());
307
308 final String [] comparableTokens = comparablePath.split(Folder.REGEX_DELIMITER);
309
310 if (comparableTokens.length == 2 && root.getComparableName().equals(comparableTokens[0]))
311 destinationFile = this.root;
312 else
313 destinationFile = this.root.get(comparableTokens, 1, comparableTokens.length - 1);
314
315 if (destinationFile == null)
316 throw new Exception("Destination folder " + destinationFile + " does not exists");
317
318 if (!destinationFile.isFolder())
319 throw new Exception("Destination " + destinationFile + " is not a folder");
320
321 final Folder destinationFolder = (Folder) destinationFile;
322
323 if (this.roBlocks.containsKey(path) || this.tempBlocks.containsKey(path))
324 throw new Exception("Block path " + path + " already exist");
325
326 final String name = properties.get(Block.NAME_LABEL);
327
328 if (name == null)
329 throw new IllegalArgumentException("Property " + Block.NAME_LABEL + " is missing");
330
331 final Cipher cipher = getCipher();
332
333 if (probe.isCancelRequested())
334 {
335 probe.fireCanceled();
336 throw new CancellationException();
337 }
338
339 cipher.init(Cipher.ENCRYPT_MODE, this.encryptionKey, getSecureRandom());
340
341 final RandomAccessFile temp = getTemp();
342
343 final long offset = temp.getFilePointer();
344
345 temp.write(cipher.getIV());
346
347 // write metadata
348
349 temp.writeLong(0);
350 final String metadataserial = GSON.toJson(properties);
351 final byte [] metaBuffer = metadataserial.getBytes();
352 final long metaOffset = temp.getFilePointer();
353 final long metaLength = encrypt(new ByteArrayInputStream(metaBuffer), cipher, temp, this.bufferSize, probe);
354 long position = temp.getFilePointer();
355
356 temp.seek(offset + cipher.getIV().length);
357 temp.writeLong(metaLength);
358 temp.seek(position);
359
360 // write data
361 position = temp.getFilePointer();
362 temp.writeLong(0);
363
364 final long dataOffset = temp.getFilePointer();
365
366 final long dataLength = encrypt(data, cipher, temp, this.bufferSize, probe);
367
368 temp.seek(position);
369 temp.writeLong(dataLength);
370 temp.seek(temp.length());
371
372 final Block block = new Block(path, properties, offset, temp.getFilePointer() - offset, metaOffset, metaLength, dataOffset, dataLength, destinationFolder);
373 this.tempBlocks.put(block.getComparablePath(), block);
374
375 destinationFolder.add(block);
376
377 return block;
378
379 } catch (final CancellationException e)
380 {
381 throw e;
382 } catch (final Exception e)
383 {
384 probe.fireException(e);
385 throw e;
386 } finally
387 {
388 probe.fireTerminated();
389 }
390 }
391
392 /**
393 * Delete data from the {@link Safe}. <b>Note that the data wont be deleted until a call to {@link Safe#save()} is made</b>
394 *
395 * @param path:
396 * path of the data to delete
397 */
398 public synchronized void delete(final String path)
399 {
400
401 final String comparablePath = path.toUpperCase(Environment.getLocale());
402 Block deleted = this.blocks.get(comparablePath);
403
404 if (deleted != null)
405 {
406 final Folder folder = deleted.getParent();
407 folder.remove(deleted.getName());
408 this.deletedBlocks.put(comparablePath, deleted);
409 }
410
411 deleted = this.tempBlocks.remove(comparablePath);
412
413 if (deleted != null)
414 {
415 final Folder folder = deleted.getParent();
416 folder.remove(deleted.getName());
417 this.deletedBlocks.put(comparablePath, deleted);
418 }
419 }
420
421 /**
422 * Extract data from the {@link Safe}
423 *
424 * @param block:
425 * block to extract
426 * @param outputStream:
427 * destination of extracted block
428 * @throws Exception
429 */
430 public void extract(final Block block, final OutputStream outputStream) throws Exception
431 {
432 extract(block.getPath(), outputStream);
433 }
434
435 /**
436 * Extract data from the {@link Safe}
437 *
438 * @param path:
439 * path of the block to extract
440 * @param outputStream:
441 * destination of extracted block
442 * @throws Exception
443 */
444 public synchronized void extract(String path, final OutputStream outputStream) throws Exception
445 {
446
447 path = path.toUpperCase(Environment.getLocale());
448
449 Block block = this.roBlocks.get(path);
450
451 final RandomAccessFile raf;
452 if (block == null)
453 {
454 block = this.tempBlocks.get(path);
455 raf = this.temp;
456 } else
457 raf = this.original;
458
459 if (block == null)
460 throw new Exception("Block " + path + " not found");
461
462 raf.seek(block.getOffset());
463
464 final byte [] ivBytes = new byte[this.ivLength];
465 raf.read(ivBytes);
466
467 final Cipher cipher = getCipher();
468
469 final IvParameterSpec iv = new IvParameterSpec(ivBytes);
470
471 cipher.init(Cipher.DECRYPT_MODE, this.encryptionKey, iv);
472 raf.seek(block.getDataOffset());
473 decrypt(raf, block.getDataLength(), cipher, outputStream, this.bufferSize);
474
475 }
476
477 /**
478 * Read the metadata of a {@link Block}
479 *
480 * @param block:
481 * block to read
482 * @return
483 * @throws Exception
484 */
485 public synchronized Map<String, String> readMetadata(final Block block) throws Exception
486 {
487
488 this.original.seek(block.getOffset());
489 final byte [] ivBytes = new byte[this.ivLength];
490 this.original.read(ivBytes);
491
492 final Cipher cipher = getCipher();
493
494 final IvParameterSpec iv = new IvParameterSpec(ivBytes);
495 cipher.init(Cipher.DECRYPT_MODE, this.encryptionKey, iv);
496
497 this.original.seek(block.getMetaOffset());
498
499 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
500 decrypt(this.original, block.getMetaLength(), cipher, baos, this.bufferSize);
501
502 final String metadata = new String(baos.toByteArray());
503
504 final Map<String, String> jsonMap = GSON.fromJson(metadata, MAP_STRING_STRING_TYPE);
505 return new TreeMap<>(jsonMap);
506 }
507
508 /**
509 * Discard pending modification
510 */
511 public synchronized void discardChanges() throws Exception
512 {
513
514 for (final Map.Entry<String, Block> temp : this.tempBlocks.entrySet())
515 {
516
517 Folder folder = temp.getValue().getParent();
518 folder.remove(temp.getValue().getName());
519 }
520
521 this.tempBlocks.clear();
522
523 for (final Map.Entry<String, Block> deleted : this.deletedBlocks.entrySet())
524 {
525 Folder folder = deleted.getValue().getParent();
526 folder.add(deleted.getValue());
527 }
528 this.deletedBlocks.clear();
529
530 }
531
532 /**
533 * Save the modification into the safe file. The current file is renamed and a new file is written. This is to reduce the risk of data loss. This method calls the {@link Safe#close()} before returning
534 *
535 * @return
536 * @throws Exception
537 */
538 public Safe save() throws Exception
539 {
540 return save(null);
541 }
542
543 public synchronized Safe save(TaskProbe probe) throws Exception
544 {
545
546 if (probe == null)
547 probe = TaskProbe.DULL_PROBE;
548 try
549 {
550 double progress = 0;
551 probe.fireProgress(progress);
552
553 probe.fireMessage("Creating temporary file");
554 final File newFile = Files.createTempFile(originalFile.getParentFile().toPath(), null, null).toFile();
555
556 try (RandomAccessFile destination = new RandomAccessFile(newFile, "rw"))
557 {
558
559 Cipher cipher = getCipher();
560
561 if (probe.isCancelRequested())
562 {
563 probe.fireCanceled();
564 throw new CancellationException();
565 }
566
567 destination.write(HASHER.getEmptyHash());// skip hash
568
569 // public properties
570 probe.fireMessage("Writing public header");
571
572 String json = GSON.toJson(this.publicHeader);
573
574 long previousPosition = destination.getFilePointer();
575
576 destination.writeLong(0);
577 long total = write(new ByteArrayInputStream(json.getBytes()), destination, this.bufferSize, probe);
578 destination.writeLong(0);// no data in header
579 long position = destination.getFilePointer();
580 destination.seek(previousPosition);
581 destination.writeLong(total);
582 destination.seek(position);
583
584 if (probe.isCancelRequested())
585 {
586 probe.fireCanceled();
587 throw new CancellationException();
588 }
589
590 // private properties
591 probe.fireMessage("Writing private properties");
592
593 cipher.init(Cipher.ENCRYPT_MODE, this.encryptionKey, getSecureRandom());
594 destination.write(cipher.getIV());
595
596 json = GSON.toJson(this.privateProperties);
597
598 previousPosition = destination.getFilePointer();
599 destination.writeLong(0);
600 total = encrypt(new ByteArrayInputStream(json.getBytes()), cipher, destination, this.bufferSize, probe);
601 destination.writeLong(0);// no data in header
602 position = destination.getFilePointer();
603 destination.seek(previousPosition);
604 destination.writeLong(total);
605 destination.seek(position);
606
607 if (probe.isCancelRequested())
608 {
609 probe.fireCanceled();
610 throw new CancellationException();
611 }
612
613 final double steps = this.roBlocks.size() + this.tempBlocks.size() + 1;
614 int completed = 0;
615
616 for (final Block block : this.roBlocks.values())
617 {
618 // add non deleted only
619 if (this.deletedBlocks.containsKey(block.getComparablePath()))
620 {
621 probe.fireMessage("Skipping deleted block " + block.getPath());
622 continue;
623 }
624
625 probe.fireMessage("Writing block " + block.getPath());
626 this.original.seek(block.getOffset());
627 write(this.original, block.getLength(), destination, this.bufferSize, probe);
628 completed++;
629 progress = completed / steps;
630 probe.fireProgress(progress);
631 }
632
633 final RandomAccessFile temp = getTemp();
634 for (final Block block : this.tempBlocks.values())
635 {
636
637 if (this.deletedBlocks.containsKey(block.getComparablePath()))
638 {
639 probe.fireMessage("Skipping deleted block " + block.getPath());
640 continue;
641 }
642
643 probe.fireMessage("Writing block " + block.getPath());
644 temp.seek(block.getOffset());
645
646 write(temp, block.getLength(), destination, this.bufferSize, probe);
647 completed++;
648 progress = completed / steps;
649 probe.fireProgress(progress);
650
651 }
652
653 probe.fireMessage("Computing hash");
654 final byte [] hash = computeHash(destination, cipher, this.ivLength, this.encryptionKey, this.bufferSize, probe);
655 destination.seek(0);
656 destination.write(hash);
657
658 probe.fireMessage("Closing IO streams");
659 destination.close();
660
661 close();
662
663 probe.fireMessage("Deleting old file");
664
665 if (!this.originalFile.delete())
666 throw new IOException("Unable to delete " + this.originalFile.getAbsolutePath());
667
668 probe.fireMessage("Renaming file");
669
670 if (!newFile.renameTo(this.originalFile))
671 throw new IOException("Unable to rename " + newFile.getAbsolutePath());
672
673 if (probe.isCancelRequested())
674 {
675 probe.fireCanceled();
676 throw new CancellationException();
677 }
678
679 probe.fireMessage("Opening new safe");
680 probe.fireProgress(1);
681
682 return new Safe(this.originalFile, encryptionKey, this.bufferSize);
683 }
684 } catch (final CancellationException e)
685 {
686 throw e;
687 } catch (final Exception e)
688 {
689 probe.fireException(e);
690 throw e;
691 } finally
692 {
693 probe.fireTerminated();
694 }
695
696 }
697
698 /**
699 * Compute the hash of the {@link Safe}
700 *
701 * @return
702 * @throws Exception
703 */
704 public synchronized byte [] computeHash(final TaskProbe probe) throws Exception
705 {
706 final byte [] hash = computeHash(this.original, getCipher(), this.ivLength, this.encryptionKey, this.bufferSize, probe);
707 return hash;
708 }
709
710 /**
711 * Return a copy of the hash that was in the {@link Safe}'s file
712 *
713 * @return
714 */
715 public byte [] getHash()
716 {
717 final byte [] destination = new byte[this.hash.length];
718 System.arraycopy(this.hash, 0, destination, 0, this.hash.length);
719 return destination;
720 }
721
722 private Cipher getCipher() throws Exception
723 {
724 final String encryption = this.publicHeader.get(ENCRYPTION_LABEL);
725
726 if (encryption == null)
727 throw new Exception("Public property '" + ENCRYPTION_LABEL + "' must be set");
728
729 return javax.crypto.Cipher.getInstance(encryption);
730 }
731
732 @Override
733 public synchronized void close() throws IOException
734 {
735 this.original.close();
736
737 final RandomAccessFile temp = getTemp();
738 if (temp != null)
739 {
740 temp.close();
741 tempFile.delete();
742 }
743
744 }
745
746 /**
747 * Get the properties of the {@link Safe}
748 *
749 * @return
750 */
751 public Map<String, String> getPrivateProperties()
752 {
753 return privateProperties;
754 }
755
756 /**
757 * Get the header of the {@link Safe}
758 *
759 * @return
760 */
761 public Map<String, String> getPublicHeader()
762 {
763 return publicHeader;
764 }
765
766 /**
767 * Get all {@link Block} contained in the {@link Safe}
768 *
769 * @return
770 */
771 public Map<String, Block> getBlocks()
772 {
773 return this.roBlocks;
774 }
775
776 /**
777 * Get a {@link Block} from the {@link Safe}
778 *
779 * @param path:
780 * path of the {@link Block} to retrieve
781 * @return
782 */
783 public Block getBlock(final String path)
784 {
785
786 final String comparablePath = path.toUpperCase(Environment.getLocale());
787
788 return this.roBlocks.get(comparablePath);
789
790 }
791
792 /**
793 * Get a {@link Block} from the temporary {@link Safe}
794 *
795 * @param path:
796 * path of the {@link Block} to retrieve
797 * @return
798 */
799 public Block getTempBlock(final String path)
800 {
801 final String comparablePath = path.toUpperCase(Environment.getLocale());
802
803 return this.tempBlocks.get(comparablePath);
804
805 }
806
807 /**
808 * Get all {@link Block} contained in the temporary {@link Safe}
809 *
810 * @return
811 */
812 public Map<String, Block> getTempBlocks()
813 {
814 return tempBlocks;
815 }
816
817 /**
818 * Get deleted {@link Block}
819 *
820 * @return
821 */
822 public Map<String, Block> getDeletedBlocks()
823 {
824 return deletedBlocks;
825 }
826
827 /**
828 * Get root {@link Folder}
829 *
830 * @return
831 */
832 public Folder getRootFolder()
833 {
834 return root;
835 }
836
837 public File getFile()
838 {
839 return this.originalFile;
840 }
841
842 /**
843 * Get the temporary safe file
844 *
845 * @return
846 */
847 public File getTempFile()
848 {
849 return tempFile;
850 }
851
852 /**
853 * Get the temporary safe file
854 *
855 * @return
856 */
857 public RandomAccessFile getTemp() throws IOException
858 {
859 return this.temp;
860 }
861
862 private static long encrypt(final InputStream data, final Cipher cipher, final RandomAccessFile destination, final int bufferSize, final TaskProbe probe) throws Exception
863 {
864
865 final byte [] buffer = new byte[bufferSize];
866 final byte [] bufferOut = new byte[bufferSize];
867
868 long total = 0;
869 int read;
870 while ((read = data.read(buffer)) > -1)
871 {
872
873 read = cipher.update(buffer, 0, read, bufferOut);
874 if (read == 0)
875 // data length is less than cipher block size
876 System.arraycopy(buffer, 0, bufferOut, 0, buffer.length);
877
878 total += read;
879 destination.write(bufferOut, 0, read);
880
881 if (probe.isCancelRequested())
882 {
883 probe.fireCanceled();
884 throw new CancellationException();
885 }
886
887 }
888
889 read = cipher.doFinal(bufferOut, 0);
890 destination.write(bufferOut, 0, read);
891 total += read;
892
893 return total;
894
895 }
896
897 private static void decrypt(final RandomAccessFile source, final long length, final Cipher cipher, final OutputStream destination, final int bufferSize) throws Exception
898 {
899
900 final byte [] buffer = new byte[bufferSize];
901 final byte [] bufferOut = new byte[bufferSize];
902
903 long remaining = length;
904 int read;
905 while (remaining > 0)
906 {
907 if (remaining < buffer.length)
908 read = source.read(buffer, 0, (int) remaining);
909 else
910 read = source.read(buffer, 0, buffer.length);
911
912 remaining -= read;
913
914 read = cipher.update(buffer, 0, read, bufferOut);
915 destination.write(bufferOut, 0, read);
916
917 }
918
919 read = cipher.doFinal(bufferOut, 0);
920 destination.write(bufferOut, 0, read);
921
922 }
923
924 private static long write(final InputStream data, final RandomAccessFile destination, final int bufferSize, final TaskProbe probe) throws Exception
925 {
926
927 final byte [] buffer = new byte[bufferSize];
928
929 long total = 0;
930 int read;
931 while ((read = data.read(buffer)) > -1)
932 {
933 destination.write(buffer, 0, read);
934 total += read;
935
936 if (probe.isCancelRequested())
937 {
938 probe.fireCanceled();
939 throw new CancellationException();
940 }
941 }
942
943 return total;
944
945 }
946
947 private static void write(final RandomAccessFile source, final long length, final RandomAccessFile destination, final int bufferSize, final TaskProbe probe) throws Exception
948 {
949
950 final byte [] buffer = new byte[bufferSize];
951
952 long remaining = length;
953 int read;
954 while (remaining > 0)
955 {
956 if (remaining < buffer.length)
957 read = source.read(buffer, 0, (int) remaining);
958 else
959 read = source.read(buffer, 0, buffer.length);
960
961 destination.write(buffer, 0, read);
962
963 remaining -= read;
964
965 if (probe.isCancelRequested())
966 {
967 probe.fireCanceled();
968 throw new CancellationException();
969 }
970 }
971
972 }
973
974 private static SecureRandom getSecureRandom()
975 {
976 return new SecureRandom();
977 }
978
979 /**
980 * Read the header of the {@link Safe}
981 *
982 * @param file:
983 * safe file to read
984 * @param bufferSize:
985 * size of the <code>byte</code> buffer to be used in IO operation
986 * @return
987 * @throws IOException
988 */
989 public static Map<String, String> readHeader(final File file, final int bufferSize) throws IOException
990 {
991 RandomAccessFile raf = null;
992
993 try
994 {
995 raf = new RandomAccessFile(file, "rw");
996 final byte [] buffer = new byte[bufferSize];
997 final ByteArrayOutputStream baos = new ByteArrayOutputStream(buffer.length);
998
999 raf.read(buffer, 0, HASHER.getHashLength());// skip hash
1000
1001 long length = raf.readLong();
1002 int read;
1003 while (length > 0)
1004 {
1005
1006 if (length < buffer.length)
1007 read = raf.read(buffer, 0, (int) length);
1008 else
1009 read = raf.read(buffer);
1010
1011 baos.write(buffer, 0, read);
1012 length -= read;
1013
1014 }
1015 final String header = new String(baos.toByteArray(), StandardCharsets.UTF_8);
1016 return GSON.fromJson(header, MAP_STRING_STRING_TYPE);
1017
1018 } finally
1019 {
1020 if (raf != null)
1021 raf.close();
1022 }
1023
1024 }
1025
1026 /**
1027 * Compute the hash value of {@link Safe} file
1028 *
1029 * @param safeFile
1030 * @param cipher
1031 * @param ivLength
1032 * @param encryptionKey
1033 * @param bufferSize
1034 * @return
1035 * @throws Exception
1036 */
1037 public static byte [] computeHash(final RandomAccessFile safeFile, final Cipher cipher, final int ivLength, final Key encryptionKey, final int bufferSize, TaskProbe probe) throws Exception
1038 {
1039
1040 if (probe == null)
1041 probe = TaskProbe.DULL_PROBE;
1042
1043 try
1044 {
1045 final long previousPosition = safeFile.getFilePointer();
1046
1047 final byte [] buffer = new byte[bufferSize];
1048 final byte [] bufferOut = new byte[bufferSize];
1049
1050
1051 safeFile.seek(HASHER.getHashLength());
1052 final ByteBuffer byteBuffer = ByteBuffer.allocate((int) (safeFile.length() - safeFile.getFilePointer()));
1053
1054 long length = safeFile.readLong();
1055 byteBuffer.putLong(length);
1056
1057 int read;
1058
1059 // header
1060 while (length > 0)// read header
1061 {
1062
1063 if (length < buffer.length)
1064 read = safeFile.read(buffer, 0, (int) length);
1065 else
1066 read = safeFile.read(buffer);
1067
1068 byteBuffer.put(buffer, 0, read);
1069 length -= read;
1070 }
1071
1072 byteBuffer.putLong(safeFile.readLong());// header's data length 0
1073
1074 if (probe.isCancelRequested())
1075 {
1076 probe.fireCanceled();
1077 throw new CancellationException();
1078 }
1079
1080 // properties
1081 safeFile.read(buffer, 0, ivLength);// read properties iv
1082 byteBuffer.put(buffer, 0, ivLength);
1083
1084 IvParameterSpec iv = new IvParameterSpec(Arrays.copyOf(buffer, ivLength));
1085 cipher.init(Cipher.DECRYPT_MODE, encryptionKey, iv);
1086
1087 length = safeFile.readLong();
1088 byteBuffer.putLong(length);
1089
1090 while (length > 0)// read properties
1091 {
1092 if (length < buffer.length)
1093 read = safeFile.read(buffer, 0, (int) length);
1094 else
1095 read = safeFile.read(buffer, 0, buffer.length);
1096
1097 length -= read;
1098
1099 read = cipher.update(buffer, 0, read, bufferOut);
1100 byteBuffer.put(bufferOut, 0, read);
1101
1102 }
1103
1104 read = cipher.doFinal(bufferOut, 0);
1105 byteBuffer.put(bufferOut, 0, read);
1106
1107 byteBuffer.putLong(safeFile.readLong());// properties data length 0
1108
1109 if (probe.isCancelRequested())
1110 {
1111 probe.fireCanceled();
1112 throw new CancellationException();
1113 }
1114
1115 // blocks
1116 while (safeFile.getFilePointer() < safeFile.length())
1117 {
1118 safeFile.read(buffer, 0, ivLength);// read properties iv
1119 byteBuffer.put(buffer, 0, ivLength);
1120 iv = new IvParameterSpec(Arrays.copyOf(buffer, ivLength));
1121 cipher.init(Cipher.DECRYPT_MODE, encryptionKey, iv);
1122
1123 // read metadata
1124 length = safeFile.readLong();
1125 byteBuffer.putLong(length);
1126
1127 while (length > 0)
1128 {
1129 if (length < buffer.length)
1130 read = safeFile.read(buffer, 0, (int) length);
1131 else
1132 read = safeFile.read(buffer, 0, buffer.length);
1133
1134 length -= read;
1135
1136 read = cipher.update(buffer, 0, read, bufferOut);
1137 byteBuffer.put(bufferOut, 0, read);
1138
1139 }
1140
1141 read = cipher.doFinal(bufferOut, 0);
1142 byteBuffer.put(bufferOut, 0, read);
1143
1144 if (probe.isCancelRequested())
1145 {
1146 probe.fireCanceled();
1147 throw new CancellationException();
1148 }
1149
1150 // read data
1151 length = safeFile.readLong();
1152 byteBuffer.putLong(length);
1153
1154 while (length > 0)
1155 {
1156 if (length < buffer.length)
1157 read = safeFile.read(buffer, 0, (int) length);
1158 else
1159 read = safeFile.read(buffer, 0, buffer.length);
1160
1161 length -= read;
1162
1163 read = cipher.update(buffer, 0, read, bufferOut);
1164 byteBuffer.put(bufferOut, 0, read);
1165
1166 if (probe.isCancelRequested())
1167 {
1168 probe.fireCanceled();
1169 throw new CancellationException();
1170 }
1171
1172 }
1173
1174 read = cipher.doFinal(bufferOut, 0);
1175 byteBuffer.put(bufferOut, 0, read);
1176
1177 }
1178
1179 safeFile.seek(previousPosition);
1180
1181 return HASHER.hash(byteBuffer.array());
1182
1183 } catch (final CancellationException e)
1184 {
1185 throw e;
1186 } catch (final Exception e)
1187 {
1188 probe.fireException(e);
1189 throw e;
1190 } finally
1191 {
1192 probe.fireTerminated();
1193 }
1194 }
1195
1196 /**
1197 * Create a new {@link Safe}
1198 *
1199 * @param file
1200 * @param key
1201 * @param publicHeader
1202 * @param privateProperties
1203 * @param bufferSize
1204 * @return
1205 * @throws Exception
1206 */
1207 public static Safe create(final File file, final byte [] key, final Map<String, String> publicHeader, final Map<String, String> privateProperties, final int bufferSize) throws Exception
1208 {
1209
1210 final String encryption = publicHeader.get(ENCRYPTION_LABEL);
1211
1212 if (encryption == null)
1213 throw new Exception("Public property '" + ENCRYPTION_LABEL + "' must be set");
1214
1215 if (!publicHeader.containsKey(ENCRYPTION_IV_LENGTH_LABEL))
1216 throw new Exception("Public property '" + ENCRYPTION_IV_LENGTH_LABEL + "' must be set");
1217
1218 if (!publicHeader.containsKey(PBKDF2_SALT_LABEL))
1219 throw new Exception("Public property '" + PBKDF2_SALT_LABEL + "' must be set");
1220
1221 if (!publicHeader.containsKey(PBKDF2_ITERATION_LABEL))
1222 throw new Exception("Public property '" + PBKDF2_ITERATION_LABEL + "' must be set");
1223
1224 Cipher cipher = javax.crypto.Cipher.getInstance(encryption);
1225
1226 final String keyAlgo = publicHeader.get(KEY_ALGO_LABEL);
1227 if (keyAlgo == null)
1228 throw new Exception("Public property '" + KEY_ALGO_LABEL + "' must be set");
1229
1230 final SecretKeySpec keySpec = new SecretKeySpec(key, keyAlgo);
1231
1232 if (file.exists())
1233 throw new IOException("File " + file + " already exist");
1234
1235 if (!file.createNewFile())
1236 throw new IOException("Could not create file " + file);
1237
1238 final RandomAccessFile raf = new RandomAccessFile(file, "rw");
1239
1240 long total, position, previousPosition;
1241
1242 cipher.init(Cipher.ENCRYPT_MODE, keySpec, getSecureRandom());
1243
1244 raf.write(HASHER.getEmptyHash());// global hash
1245
1246 // header
1247 position = raf.getFilePointer();
1248 // no IV in header
1249 raf.writeLong(0);
1250 final String header = GSON.toJson(publicHeader);
1251 total = write(new ByteArrayInputStream(header.getBytes(StandardCharsets.UTF_8)), raf, bufferSize, TaskProbe.DULL_PROBE);
1252 raf.writeLong(0);// no data in header block
1253
1254 previousPosition = raf.getFilePointer();
1255 raf.seek(position);
1256 raf.writeLong(total);
1257
1258 raf.seek(previousPosition);
1259
1260 // properties
1261 position = raf.getFilePointer();
1262 raf.write(cipher.getIV());
1263 final String privatePropsJson = GSON.toJson(privateProperties == null ? new HashMap<>() : privateProperties);
1264 previousPosition = raf.getFilePointer();
1265 raf.writeLong(0L);
1266 total = encrypt(new ByteArrayInputStream(privatePropsJson.getBytes(StandardCharsets.UTF_8)), cipher, raf, bufferSize, TaskProbe.DULL_PROBE);
1267 raf.writeLong(0);// no data in properties block
1268
1269 raf.seek(previousPosition);
1270 raf.writeLong(total);
1271
1272 // write global hash
1273 final byte [] hash = computeHash(raf, cipher, cipher.getIV().length, keySpec, bufferSize, null);
1274 raf.seek(0);
1275 raf.write(hash);
1276
1277 raf.close();
1278
1279 return new Safe(file, keySpec, bufferSize);
1280
1281 }
1282
1283}
1284
1285/*******************************************************************************
1286 * Copyright 2018 Ortis (cao.ortis.org@gmail.com)
1287 *
1288 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
1289 *
1290 * http://www.apache.org/licenses/LICENSE-2.0
1291 *
1292 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1293 * License for the specific language governing permissions and limitations under the License.
1294 ******************************************************************************/
1295
1296package org.ortis.jsafebox;
1297
1298import java.awt.GraphicsDevice;
1299import java.awt.GraphicsEnvironment;
1300import java.awt.HeadlessException;
1301import java.io.File;
1302import java.io.IOException;
1303import java.nio.ByteBuffer;
1304import java.nio.CharBuffer;
1305import java.nio.charset.StandardCharsets;
1306import java.nio.file.FileSystems;
1307import java.nio.file.FileVisitResult;
1308import java.nio.file.FileVisitor;
1309import java.nio.file.Files;
1310import java.nio.file.Path;
1311import java.nio.file.PathMatcher;
1312import java.nio.file.Paths;
1313import java.nio.file.attribute.BasicFileAttributes;
1314import java.util.Arrays;
1315import java.util.List;
1316import java.util.Map;
1317import java.util.logging.Logger;
1318import java.util.regex.Pattern;
1319
1320import javax.crypto.SecretKeyFactory;
1321import javax.crypto.spec.PBEKeySpec;
1322import javax.crypto.spec.SecretKeySpec;
1323
1324/**
1325 * Utility class
1326 *
1327 * @author Ortis <br>
1328 * 2018 Apr 26 8:06:47 PM <br>
1329 */
1330public class Utils
1331{
1332
1333 public final static String SEPARATOR_REGEX = "[/|" + Pattern.quote(java.io.File.separator) + "]";
1334
1335 private final static String SYSTEM_PATH_DELIMITER_REGEX = Pattern.quote(File.separator) + "|" + Pattern.quote("/") + "|" + Pattern.quote("\");
1336
1337 public static byte [] passwordToBytes(final char [] chars)
1338 {
1339 final CharBuffer charBuffer = CharBuffer.wrap(chars);
1340 final ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer);
1341 final byte [] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit());
1342 Arrays.fill(charBuffer.array(), 'u0000'); // clear sensitive data
1343 Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data
1344 return bytes;
1345 }
1346
1347 /**
1348 * Open a {@link Safe}
1349 *
1350 * @param safeFilePath:
1351 * system path to the safe file
1352 * @param password:
1353 * the encryption password
1354 * @param bufferSize:size
1355 * of the <code>byte</code> buffer to be used in IO operation
1356 * @param log
1357 * @return
1358 * @throws Exception
1359 */
1360 public static Safe open(final String safeFilePath, final char [] password, final int bufferSize, final Logger log) throws Exception
1361 {
1362 final File file = new File(safeFilePath);
1363
1364 if (!file.exists())
1365 throw new IOException("Safe file " + file + " doest not exist");
1366
1367 final Map<String, String> header = Safe.readHeader(file, bufferSize);
1368
1369 final String encyption = header.get(Safe.ENCRYPTION_LABEL);
1370 if (encyption == null)
1371 throw new Exception("Could not read property '" + Safe.ENCRYPTION_LABEL + "' from header");
1372
1373 if (log != null)
1374 log.fine("Encryption type " + encyption);
1375
1376 if (!header.containsKey(Safe.KEY_ALGO_LABEL))
1377 throw new Exception("Could not read property '" + Safe.KEY_ALGO_LABEL + "' from header");
1378
1379 if (log != null)
1380 log.fine("Key algorithm " + header.get(Safe.KEY_ALGO_LABEL));
1381
1382
1383 final byte [] salt = (byte [])Safe.GSON.fromJson(header.get(Safe.PBKDF2_SALT_LABEL), Safe.BYTE_ARRAY_TYPE);
1384
1385 PBEKeySpec spec = new PBEKeySpec(password, salt, Safe.PBKDF2_ITERATION, 128);
1386 SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
1387 final byte [] key = skf.generateSecret(spec).getEncoded();
1388
1389 final SecretKeySpec keySpec = new SecretKeySpec(key, header.get(Safe.KEY_ALGO_LABEL));
1390
1391 return new Safe(file, keySpec, bufferSize);
1392 }
1393
1394 public static List<java.io.File> parseSystemPath(String query, final List<java.io.File> destination) throws IOException
1395 {
1396 final String [] tokens = query.split(SYSTEM_PATH_DELIMITER_REGEX);
1397
1398 Path baseDirectory = null;
1399
1400 if (tokens[0].equals(".") || tokens[0].equals(".."))
1401 {
1402 baseDirectory = new File(tokens[0]).toPath();
1403
1404 final StringBuilder sb = new StringBuilder();
1405 for (int i = 1; i < tokens.length; i++)
1406 if (sb.length() == 0)
1407 sb.append(tokens[i]);
1408 else
1409 sb.append(File.separator + tokens[i]);
1410
1411 query = "**" + File.separator + sb.toString();
1412
1413 } else
1414 {
1415
1416 final String comparableToken = tokens[0].toUpperCase();
1417 for (final File root : File.listRoots())
1418 if (root.getAbsolutePath().toUpperCase().equals(comparableToken))
1419 {
1420 // perfect match
1421 baseDirectory = root.toPath();
1422 break;
1423
1424 }
1425
1426 if (baseDirectory == null)
1427 for (final File root : File.listRoots())
1428 {
1429 String rootPath = root.getAbsolutePath().toUpperCase();
1430 rootPath = rootPath.substring(0, rootPath.length() - 1);
1431 if (rootPath.equals(comparableToken))
1432 {
1433 baseDirectory = root.toPath();
1434 break;
1435 }
1436
1437 }
1438 }
1439
1440 if (baseDirectory == null)
1441 throw new IOException("Could not locate base directory '" + tokens[0] + "'");
1442
1443 Path path = baseDirectory;
1444 for (int i = 1; i < tokens.length; i++)
1445 {
1446
1447 try
1448 {
1449 path = Paths.get(path.toString(), tokens[i]);
1450 } catch (final Exception e)
1451 {
1452 // here, we have reach a special character and the start point for the search is
1453 // in path
1454 }
1455 }
1456
1457 final String escapedQuery = query.replace("\", "\\");// PathMatcher does not escape backslash properly. Need to do the escape manually for Windows OS path handling. This might be a bug of Java implentation.
1458 // Need to check on Oracle bug report database.
1459
1460 final PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:" + escapedQuery);
1461 Files.walkFileTree(path, new FileVisitor<Path>()
1462 {
1463
1464 @Override
1465 public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) throws IOException
1466 {
1467
1468 return FileVisitResult.CONTINUE;
1469 }
1470
1471 @Override
1472 public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs) throws IOException
1473 {
1474 if (pathMatcher.matches(dir))
1475 {
1476 destination.add(dir.toFile());
1477 return FileVisitResult.SKIP_SUBTREE;
1478 }
1479
1480 return FileVisitResult.CONTINUE;
1481 }
1482
1483 @Override
1484 public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException
1485 {
1486
1487 if (pathMatcher.matches(file))
1488 destination.add(file.toFile());
1489
1490 return FileVisitResult.CONTINUE;
1491 }
1492
1493 @Override
1494 public FileVisitResult visitFileFailed(final Path file, final IOException exc) throws IOException
1495 {
1496 return FileVisitResult.CONTINUE;
1497 }
1498 });
1499
1500 return destination;
1501
1502 }
1503
1504 /**
1505 * Return the MIME type of a file
1506 *
1507 * @param file
1508 * @return
1509 */
1510 public static String getMIMEType(final java.io.File file)
1511 {
1512
1513 final String name = file.getName().toUpperCase();
1514
1515 if (name.endsWith(".TXT"))
1516 return "text/plain";
1517 else if (name.endsWith(".CSV"))
1518 return "text/csv";
1519 else if (name.endsWith(".HTM") || name.endsWith(".HTML"))
1520 return "text/html";
1521 else if (name.endsWith(".JPG") || name.endsWith(".JPEG"))
1522 return "image/jpg";
1523 else if (name.endsWith(".PNG"))
1524 return "image/png";
1525 else if (name.endsWith(".BM") || name.endsWith(".BMP"))
1526 return "image/bmp";
1527 else if (name.endsWith(".PDF"))
1528 return "application/pdf";
1529 else if (name.endsWith(".AVI"))
1530 return "video/x-msvideo";
1531 else if (name.endsWith(".MPEG"))
1532 return "video/mpeg";
1533 else if (name.endsWith(".MP4"))
1534 return "video/mp4";
1535 else if (name.endsWith(".MKV"))
1536 return "video/x-matroska";
1537 else if (name.endsWith(".MP3"))
1538 return "audio/mpeg";
1539 else
1540 return "application/octet-stream";
1541 }
1542
1543 /**
1544 * Format the exception message
1545 *
1546 * @param t
1547 * @return
1548 */
1549 public static String formatException(final Throwable t)
1550 {
1551 if (t == null)
1552 return null;
1553
1554 final Throwable cause = t.getCause();
1555 final String msg = cause == null ? null : formatException(cause);
1556 return formatException(t.getClass(), msg, t.toString(), t.getStackTrace());
1557
1558 }
1559
1560 private static String formatException(final Class<?> exceptionClass, final String cause, final String msg, final StackTraceElement [] exceptionStack)
1561 {
1562 final StringBuilder builder = new StringBuilder();
1563
1564 if (msg != null)
1565 builder.append(msg);
1566
1567 if (exceptionStack != null)
1568 {
1569 builder.append(System.lineSeparator());
1570 for (int i = 0; i < exceptionStack.length; i++)
1571 {
1572 final String stackElement = exceptionStack[i].toString();
1573
1574 builder.append(stackElement + System.lineSeparator());
1575 }
1576 }
1577
1578 if (cause != null)
1579 builder.append("Caused by " + cause);
1580
1581 return builder.toString();
1582 }
1583
1584 /**
1585 * Remove forbidden <code>char</code> from the path and replace them with <code>substitute</code>
1586 *
1587 * @param path:
1588 * the path to sanitize
1589 * @param delimiter:
1590 * delimiter of the path
1591 * @param substitute:
1592 * replacement char
1593 * @return
1594 */
1595 public static String sanitize(final String path, final Character delimiter, final Character substitute)
1596 {
1597 final String [] tokens = path.split(Pattern.quote(Character.toString(delimiter)));
1598
1599 final StringBuilder sb = new StringBuilder();
1600 for (int i = 0; i < tokens.length; i++)
1601 {
1602 if (i < tokens.length - 1)
1603 sb.append(sanitizeToken(tokens[i], substitute) + delimiter);
1604 else
1605 sb.append(sanitizeToken(tokens[i], substitute));
1606
1607 }
1608
1609 return sb.toString();
1610 }
1611
1612 public static String sanitizeToken(final String token, final Character substitute)
1613 {
1614
1615 final StringBuilder sb = new StringBuilder(token);
1616
1617 final Character replacement = substitute;
1618
1619 c: for (int i = 0; i < sb.length(); i++)
1620 {
1621
1622 if (sb.charAt(i) == java.io.File.separatorChar || sb.charAt(i) == Folder.DELIMITER)
1623 {
1624 if (replacement == null)
1625 sb.deleteCharAt(i--);
1626 else
1627 sb.setCharAt(i, replacement);
1628 continue c;
1629 }
1630
1631 for (final char c : Environment.getForbidenChars())
1632 if (sb.charAt(i) == c)
1633 {
1634 if (replacement == null)
1635 sb.deleteCharAt(i--);
1636 else
1637 sb.setCharAt(i, replacement);
1638 continue c;
1639 }
1640 }
1641 return sb.toString();
1642
1643 }
1644
1645 public static boolean isHeadless()
1646 {
1647 if (GraphicsEnvironment.isHeadless())
1648 return true;
1649
1650 try
1651 {
1652 GraphicsDevice [] screenDevices = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices();
1653 return screenDevices == null || screenDevices.length == 0;
1654 } catch (HeadlessException e)
1655 {
1656 return true;
1657 }
1658 }
1659
1660}