package peergos.shared.user.fs;
import peergos.shared.*;
import peergos.shared.cbor.*;
import peergos.shared.crypto.*;
import peergos.shared.crypto.random.*;
import peergos.shared.crypto.symmetric.*;
import peergos.shared.io.ipfs.multihash.*;
import peergos.shared.user.*;
import peergos.shared.util.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.*;
public class EncryptedChunkRetriever implements FileRetriever {
private final byte[] chunkNonce, chunkAuth;
private final List<Multihash> fragmentHashes;
private final Optional<CipherText> nextChunk;
private final Fragmenter fragmenter;
public EncryptedChunkRetriever(byte[] chunkNonce,
byte[] chunkAuth,
List<Multihash> fragmentHashes,
Optional<CipherText> nextChunk,
Fragmenter fragmenter) {
this.chunkNonce = chunkNonce;
this.chunkAuth = chunkAuth;
this.fragmentHashes = fragmentHashes;
this.nextChunk = nextChunk;
this.fragmenter = fragmenter;
}
@Override
public CompletableFuture<AsyncReader> getFile(NetworkAccess network, SafeRandom random, SymmetricKey dataKey, long fileSize,
Location ourLocation, ProgressConsumer<Long> monitor) {
return getChunkInputStream(network, random, dataKey, 0, fileSize, ourLocation, monitor)
.thenApply(chunk -> {
Location nextChunkPointer = this.getNext(dataKey).orElse(null);
return new LazyInputStreamCombiner(0,
chunk.get().chunk.data(), nextChunkPointer,
chunk.get().chunk.data(), nextChunkPointer,
network, random, dataKey, fileSize, monitor);
});
}
public CompletableFuture<Optional<LocatedEncryptedChunk>> getEncryptedChunk(long bytesRemainingUntilStart,
long truncateTo,
byte[] nonce,
SymmetricKey dataKey,
Location ourLocation,
NetworkAccess network,
ProgressConsumer<Long> monitor) {
if (bytesRemainingUntilStart < Chunk.MAX_SIZE) {
return network.downloadFragments(fragmentHashes, monitor, fragmenter.storageIncreaseFactor()).thenCompose(fragments -> {
fragments = reorder(fragments, fragmentHashes);
byte[][] collect = fragments.stream().map(f -> f.fragment.data).toArray(byte[][]::new);
byte[] cipherText = fragmenter.recombine(collect, Chunk.MAX_SIZE);
EncryptedChunk fullEncryptedChunk = new EncryptedChunk(ArrayOps.concat(chunkAuth, cipherText));
if (truncateTo < Chunk.MAX_SIZE)
fullEncryptedChunk = fullEncryptedChunk.truncateTo((int) truncateTo);
return CompletableFuture.completedFuture(Optional.of(new LocatedEncryptedChunk(ourLocation, fullEncryptedChunk, nonce)));
});
}
Optional<Location> next = getNext(dataKey);
if (! next.isPresent())
return CompletableFuture.completedFuture(Optional.empty());
return network.getMetadata(next.get()).thenCompose(meta ->
!meta.isPresent() ? CompletableFuture.completedFuture(Optional.empty()) :
meta.get().retriever().getEncryptedChunk(bytesRemainingUntilStart - Chunk.MAX_SIZE,
truncateTo - Chunk.MAX_SIZE, meta.get().retriever().getNonce(), dataKey, next.get(), network, monitor)
);
}
public CompletableFuture<Optional<Location>> getLocationAt(Location startLocation, long offset, SymmetricKey dataKey, NetworkAccess network) {
if (offset < Chunk.MAX_SIZE)
return CompletableFuture.completedFuture(Optional.of(startLocation));
Optional<Location> next = getNext(dataKey);
if (! next.isPresent())
return CompletableFuture.completedFuture(Optional.empty());
if (offset < 2*Chunk.MAX_SIZE)
return CompletableFuture.completedFuture(next); // chunk at this location hasn't been written yet, only referenced by previous chunk
return network.getMetadata(next.get())
.thenCompose(meta -> meta.isPresent() ?
meta.get().retriever().getLocationAt(next.get(), offset - Chunk.MAX_SIZE, dataKey, network) :
CompletableFuture.completedFuture(Optional.empty())
);
}
public Optional<Location> getNext(SymmetricKey dataKey) {
return this.nextChunk.map(c -> c.decrypt(dataKey, raw -> Location.fromByteArray(raw)));
}
public byte[] getNonce() {
return chunkNonce;
}
public CompletableFuture<Optional<LocatedChunk>> getChunkInputStream(NetworkAccess network, SafeRandom random,
SymmetricKey dataKey,
long startIndex, long truncateTo,
Location ourLocation, ProgressConsumer<Long> monitor) {
return getEncryptedChunk(startIndex, truncateTo, chunkNonce, dataKey, ourLocation, network, monitor).thenCompose(fullEncryptedChunk -> {
if (!fullEncryptedChunk.isPresent()) {
return getLocationAt(ourLocation, startIndex, dataKey, network).thenApply(unwrittenChunkLocation ->
!unwrittenChunkLocation.isPresent() ? Optional.empty() :
Optional.of(new LocatedChunk(unwrittenChunkLocation.get(),
new Chunk(new byte[Math.min(Chunk.MAX_SIZE, (int) (truncateTo - startIndex))],
dataKey, unwrittenChunkLocation.get().getMapKey(),
random.randomBytes(TweetNaCl.SECRETBOX_NONCE_BYTES)))));
}
if (!fullEncryptedChunk.isPresent())
return CompletableFuture.completedFuture(Optional.empty());
try {
byte[] original = fullEncryptedChunk.get().chunk.decrypt(dataKey, fullEncryptedChunk.get().nonce);
return CompletableFuture.completedFuture(Optional.of(new LocatedChunk(fullEncryptedChunk.get().location,
new Chunk(original, dataKey, fullEncryptedChunk.get().location.getMapKey(), random.randomBytes(TweetNaCl.SECRETBOX_NONCE_BYTES)))));
} catch (IllegalStateException e) {
throw new IllegalStateException("Couldn't decrypt chunk at mapkey: " + new ByteArrayWrapper(fullEncryptedChunk.get().location.getMapKey()), e);
}
});
}
@Override
public CborObject toCbor() {
return new CborObject.CborList(Arrays.asList(
new CborObject.CborByteArray(chunkNonce),
new CborObject.CborByteArray(chunkAuth),
new CborObject.CborList(fragmentHashes
.stream()
.map(CborObject.CborMerkleLink::new)
.collect(Collectors.toList())),
! nextChunk.isPresent() ? new CborObject.CborNull() : nextChunk.get().toCbor(),
fragmenter.toCbor()
));
}
public static EncryptedChunkRetriever fromCbor(CborObject cbor) {
if (! (cbor instanceof CborObject.CborList))
throw new IllegalStateException("Incorrect cbor for EncryptedChunkRetriever: " + cbor);
List<CborObject> value = ((CborObject.CborList) cbor).value;
byte[] chunkNonce = ((CborObject.CborByteArray)value.get(0)).value;
byte[] chunkAuth = ((CborObject.CborByteArray)value.get(1)).value;
List<Multihash> fragmentHashes = ((CborObject.CborList)value.get(2)).value
.stream()
.map(c -> ((CborObject.CborMerkleLink)c).target)
.collect(Collectors.toList());
Optional<CipherText> nextChunk = value.get(3) instanceof CborObject.CborNull ? Optional.empty() : Optional.of(CipherText.fromCbor(value.get(3)));
Fragmenter fragmenter = Fragmenter.fromCbor(value.get(4));
return new EncryptedChunkRetriever(chunkNonce, chunkAuth, fragmentHashes, nextChunk, fragmenter);
}
private static List<FragmentWithHash> reorder(List<FragmentWithHash> fragments, List<Multihash> hashes) {
FragmentWithHash[] res = new FragmentWithHash[fragments.size()];
for (FragmentWithHash f: fragments) {
for (int index = 0; index < res.length; index++)
if (hashes.get(index).equals(f.hash))
res[index] = f;
}
return Arrays.asList(res);
}
private static List<byte[]> split(byte[] arr, int size) {
int length = arr.length/size;
List<byte[]> res = new ArrayList<>();
for (int i=0; i < length; i++)
res.add(Arrays.copyOfRange(arr, i*size, (i+1)*size));
return res;
}
}