package peergos.shared.user.fs;
import peergos.shared.*;
import peergos.shared.cbor.*;
import peergos.shared.crypto.*;
import peergos.shared.crypto.asymmetric.*;
import peergos.shared.crypto.random.*;
import peergos.shared.crypto.symmetric.*;
import peergos.shared.user.*;
import peergos.shared.util.*;
import java.io.*;
import java.time.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.*;
public class DirAccess extends FileAccess {
public static final int MAX_CHILD_LINKS_PER_BLOB = 500;
private final SymmetricLink subfolders2files, subfolders2parent;
private List<SymmetricLocationLink> subfolders, files;
private final Optional<SymmetricLocationLink> moreFolderContents;
public DirAccess(SymmetricLink subfolders2files, SymmetricLink subfolders2parent, List<SymmetricLocationLink> subfolders,
List<SymmetricLocationLink> files, SymmetricLink parent2meta, byte[] properties,
FileRetriever retriever, SymmetricLocationLink parentLink, Optional<SymmetricLocationLink> moreFolderContents) {
super(parent2meta, properties, retriever, parentLink);
this.subfolders2files = subfolders2files;
this.subfolders2parent = subfolders2parent;
this.subfolders = subfolders;
this.files = files;
this.moreFolderContents = moreFolderContents;
}
public DirAccess withNextBlob(Optional<SymmetricLocationLink> moreFolderContents) {
return new DirAccess(subfolders2files, subfolders2parent, subfolders, files, parent2meta, properties,
retriever, parentLink, moreFolderContents);
}
@Override
public CborObject toCbor() {
CborObject file = super.toCbor();
CborObject dirPart = new CborObject.CborList(Arrays.asList(
subfolders2parent.toCbor(),
subfolders2files.toCbor(),
new CborObject.CborList(subfolders
.stream()
.map(locLink -> locLink.toCbor())
.collect(Collectors.toList())),
new CborObject.CborList(files
.stream()
.map(locLink -> locLink.toCbor())
.collect(Collectors.toList())),
moreFolderContents.isPresent() ? moreFolderContents.get().toCbor() : new CborObject.CborNull()
));
return new CborObject.CborList(Arrays.asList(file, dirPart));
}
public static DirAccess fromCbor(CborObject cbor, FileAccess base) {
if (! (cbor instanceof CborObject.CborList))
throw new IllegalStateException("Incorrect cbor for DirAccess: " + cbor);
List<CborObject> value = ((CborObject.CborList) cbor).value;
SymmetricLink subfoldersToParent = SymmetricLink.fromCbor(value.get(0));
SymmetricLink subfoldersToFiles = SymmetricLink.fromCbor(value.get(1));
List<SymmetricLocationLink> subfolders = ((CborObject.CborList)value.get(2)).value
.stream()
.map(SymmetricLocationLink::fromCbor)
.collect(Collectors.toList());
List<SymmetricLocationLink> files = ((CborObject.CborList)value.get(3)).value
.stream()
.map(SymmetricLocationLink::fromCbor)
.collect(Collectors.toList());
Optional<SymmetricLocationLink> moreFolderContents = value.get(4) instanceof CborObject.CborNull ?
Optional.empty() : Optional.of(SymmetricLocationLink.fromCbor(value.get(4)));
return new DirAccess(subfoldersToFiles, subfoldersToParent, subfolders, files,
base.parent2meta, base.properties, base.retriever, base.parentLink, moreFolderContents);
}
public List<SymmetricLocationLink> getSubfolders() {
return Collections.unmodifiableList(subfolders);
}
public List<SymmetricLocationLink> getFiles() {
return Collections.unmodifiableList(files);
}
public boolean isDirty(SymmetricKey baseKey) {
throw new IllegalStateException("Unimplemented!");
}
public CompletableFuture<Boolean> rename(FilePointer writableFilePointer, FileProperties newProps, NetworkAccess network) {
if (!writableFilePointer.isWritable())
throw new IllegalStateException("Need a writable pointer!");
SymmetricKey metaKey;
SymmetricKey parentKey = subfolders2parent.target(writableFilePointer.baseKey);
metaKey = this.getMetaKey(parentKey);
byte[] metaNonce = metaKey.createNonce();
DirAccess dira = new DirAccess(this.subfolders2files, this.subfolders2parent,
this.subfolders, this.files, this.parent2meta,
ArrayOps.concat(metaNonce, metaKey.encrypt(newProps.serialize(), metaNonce)),
null,
parentLink,
moreFolderContents
);
return network.uploadChunk(dira, writableFilePointer.location, writableFilePointer.signer());
}
public CompletableFuture<DirAccess> addFileAndCommit(FilePointer targetCAP, SymmetricKey ourSubfolders,
FilePointer ourPointer, SigningKeyPair signer,
NetworkAccess network, SafeRandom random) {
return addFilesAndCommit(Arrays.asList(targetCAP), ourSubfolders, ourPointer, signer, network, random);
}
public CompletableFuture<DirAccess> addFilesAndCommit(List<FilePointer> targetCAPs, SymmetricKey ourSubfolders,
FilePointer ourPointer, SigningKeyPair signer,
NetworkAccess network, SafeRandom random) {
if (subfolders.size() + files.size() + targetCAPs.size() > MAX_CHILD_LINKS_PER_BLOB) {
return getNextMetablob(ourSubfolders, network).thenCompose(nextMetablob -> {
if (nextMetablob.size() >= 1) {
FilePointer nextPointer = nextMetablob.get(0).filePointer;
DirAccess nextBlob = (DirAccess) nextMetablob.get(0).fileAccess;
return nextBlob.addFilesAndCommit(targetCAPs, ourSubfolders, nextPointer, signer, network, random);
} else {
// first fill this directory, then overflow into a new one
int freeSlots = MAX_CHILD_LINKS_PER_BLOB - subfolders.size() - files.size();
List<FilePointer> addToUs = targetCAPs.subList(0, freeSlots);
List<FilePointer> addToNext = targetCAPs.subList(freeSlots, targetCAPs.size());
return addFilesAndCommit(addToUs, ourSubfolders, ourPointer, signer, network, random)
.thenCompose(newUs -> {
// create and upload new metadata blob
SymmetricKey nextSubfoldersKey = SymmetricKey.random();
SymmetricKey ourParentKey = subfolders2parent.target(ourSubfolders);
DirAccess next = DirAccess.create(nextSubfoldersKey, FileProperties.EMPTY,
parentLink.targetLocation(ourParentKey), parentLink.target(ourParentKey), ourParentKey);
byte[] nextMapKey = random.randomBytes(32);
Location nextLocation = ourPointer.getLocation().withMapKey(nextMapKey);
FilePointer nextPointer = new FilePointer(nextLocation, Optional.empty(), nextSubfoldersKey);
return next.addFilesAndCommit(addToNext, nextSubfoldersKey, nextPointer, signer, network, random)
.thenCompose(nextBlob -> {
// re-upload us with the link to the next DirAccess
DirAccess withNext = newUs.withNextBlob(Optional.of(
SymmetricLocationLink.create(ourSubfolders,
nextSubfoldersKey, nextPointer.getLocation())));
return withNext.commit(ourPointer.getLocation(), signer, network);
});
});
}
});
} else {
SymmetricKey filesKey = this.subfolders2files.target(ourSubfolders);
for (FilePointer targetCAP : targetCAPs)
this.files.add(SymmetricLocationLink.create(filesKey, targetCAP.baseKey, targetCAP.getLocation()));
return commit(ourPointer.getLocation(), signer, network)
.thenApply(x -> this);
}
}
public CompletableFuture<DirAccess> addSubdirAndCommit(FilePointer targetCAP, SymmetricKey ourSubfolders,
FilePointer ourPointer, SigningKeyPair signer,
NetworkAccess network, SafeRandom random) {
return addSubdirsAndCommit(Arrays.asList(targetCAP), ourSubfolders, ourPointer, signer, network, random);
}
// returns new version of this directory
public CompletableFuture<DirAccess> addSubdirsAndCommit(List<FilePointer> targetCAPs, SymmetricKey ourSubfolders,
FilePointer ourPointer, SigningKeyPair signer,
NetworkAccess network, SafeRandom random) {
if (subfolders.size() + files.size() + targetCAPs.size() > MAX_CHILD_LINKS_PER_BLOB) {
return getNextMetablob(ourSubfolders, network).thenCompose(nextMetablob -> {
if (nextMetablob.size() >= 1) {
FilePointer nextPointer = nextMetablob.get(0).filePointer;
DirAccess nextBlob = (DirAccess) nextMetablob.get(0).fileAccess;
return nextBlob.addSubdirsAndCommit(targetCAPs, nextPointer.baseKey,
nextPointer.withWritingKey(ourPointer.location.writer), signer, network, random);
} else {
// first fill this directory, then overflow into a new one
int freeSlots = MAX_CHILD_LINKS_PER_BLOB - subfolders.size() - files.size();
List<FilePointer> addToUs = targetCAPs.subList(0, freeSlots);
List<FilePointer> addToNext = targetCAPs.subList(freeSlots, targetCAPs.size());
return addSubdirsAndCommit(addToUs, ourSubfolders, ourPointer, signer, network, random).thenCompose(newUs -> {
// create and upload new metadata blob
SymmetricKey nextSubfoldersKey = SymmetricKey.random();
SymmetricKey ourParentKey = subfolders2parent.target(ourSubfolders);
DirAccess next = DirAccess.create(nextSubfoldersKey, FileProperties.EMPTY,
parentLink != null ? parentLink.targetLocation(ourParentKey) : null,
parentLink != null ? parentLink.target(ourParentKey) : null, ourParentKey);
byte[] nextMapKey = random.randomBytes(32);
FilePointer nextPointer = new FilePointer(ourPointer.location.withMapKey(nextMapKey), Optional.empty(), nextSubfoldersKey);
return next.addSubdirsAndCommit(addToNext, nextSubfoldersKey, nextPointer, signer, network, random)
.thenCompose(x -> {
// re-upload us with the link to the next DirAccess
DirAccess withNextBlob = newUs.withNextBlob(Optional.of(
SymmetricLocationLink.create(ourSubfolders,
nextSubfoldersKey, nextPointer.getLocation())));
return withNextBlob.commit(ourPointer.getLocation(), signer, network);
});
});
}
});
} else {
for (FilePointer targetCAP : targetCAPs)
this.subfolders.add(SymmetricLocationLink.create(ourSubfolders, targetCAP.baseKey, targetCAP.getLocation()));
return commit(ourPointer.getLocation(), signer, network);
}
}
private CompletableFuture<List<RetrievedFilePointer>> getNextMetablob(SymmetricKey subfoldersKey, NetworkAccess network) {
if (!moreFolderContents.isPresent())
return CompletableFuture.completedFuture(Collections.emptyList());
return network.retrieveAllMetadata(Arrays.asList(moreFolderContents.get()), subfoldersKey);
}
public CompletableFuture<Boolean> updateChildLink(FilePointer ourPointer, RetrievedFilePointer original,
RetrievedFilePointer modified, SigningKeyPair signer,
NetworkAccess network, SafeRandom random) {
removeChild(original, ourPointer, signer, network);
CompletableFuture<DirAccess> toUpdate;
if (modified.fileAccess.isDirectory())
toUpdate = addSubdirAndCommit(modified.filePointer, ourPointer.baseKey, ourPointer, signer, network, random);
else {
toUpdate = addFileAndCommit(modified.filePointer, ourPointer.baseKey, ourPointer, signer, network, random);
}
return toUpdate.thenCompose(newDirAccess -> network.uploadChunk(newDirAccess, ourPointer.getLocation(), ourPointer.signer()));
}
public CompletableFuture<Boolean> removeChild(RetrievedFilePointer childRetrievedPointer, FilePointer ourPointer,
SigningKeyPair signer, NetworkAccess network) {
if (childRetrievedPointer.fileAccess.isDirectory()) {
this.subfolders = subfolders.stream().filter(e -> {
try {
Location target = e.targetLocation(ourPointer.baseKey);
boolean keep = true;
if (Arrays.equals(target.getMapKey(), childRetrievedPointer.filePointer.location.getMapKey()))
if (Arrays.equals(target.writer.serialize(), childRetrievedPointer.filePointer.location.writer.serialize()))
if (Arrays.equals(target.owner.serialize(), childRetrievedPointer.filePointer.location.owner.serialize()))
keep = false;
return keep;
} catch (TweetNaCl.InvalidCipherTextException ex) {
ex.printStackTrace();
return false;
} catch (Exception f) {
return false;
}
}).collect(Collectors.toList());
} else {
files = files.stream().filter(e -> {
SymmetricKey filesKey = subfolders2files.target(ourPointer.baseKey);
try {
Location target = e.targetLocation(filesKey);
boolean keep = true;
if (Arrays.equals(target.getMapKey(), childRetrievedPointer.filePointer.location.getMapKey()))
if (Arrays.equals(target.writer.serialize(), childRetrievedPointer.filePointer.location.writer.serialize()))
if (Arrays.equals(target.owner.serialize(), childRetrievedPointer.filePointer.location.owner.serialize()))
keep = false;
return keep;
} catch (TweetNaCl.InvalidCipherTextException ex) {
ex.printStackTrace();
return false;
} catch (Exception f) {
return false;
}
}).collect(Collectors.toList());
}
return network.uploadChunk(this, ourPointer.getLocation(), signer);
}
// 0=FILE, 1=DIR
public byte getType() {
return 1;
}
// returns [RetrievedFilePointer]
public CompletableFuture<Set<RetrievedFilePointer>> getChildren(NetworkAccess network, SymmetricKey baseKey) {
CompletableFuture<List<RetrievedFilePointer>> subdirsFuture = network.retrieveAllMetadata(this.subfolders, baseKey);
CompletableFuture<List<RetrievedFilePointer>> filesFuture = network.retrieveAllMetadata(this.files, this.subfolders2files.target(baseKey));
CompletableFuture<List<RetrievedFilePointer>> moreChildrenFuture = moreFolderContents.isPresent() ?
network.retrieveAllMetadata(Arrays.asList(moreFolderContents.get()), baseKey) :
CompletableFuture.completedFuture(Collections.emptyList());
return subdirsFuture.thenCompose(subdirs -> filesFuture.thenCompose(files -> moreChildrenFuture.thenCompose(moreChildrenSource -> {
// this only has one or zero elements
Optional<RetrievedFilePointer> any = moreChildrenSource.stream().findAny();
CompletableFuture<Set<RetrievedFilePointer>> moreChildren = any.map(d -> ((DirAccess)d.fileAccess).getChildren(network, d.filePointer.baseKey))
.orElse(CompletableFuture.completedFuture(Collections.emptySet()));
return moreChildren.thenApply(moreRetrievedChildren -> {
Set<RetrievedFilePointer> results = Stream.concat(
Stream.concat(
subdirs.stream(),
files.stream()),
moreRetrievedChildren.stream())
.collect(Collectors.toSet());
return results;
});
})
)
);
}
public CompletableFuture<Boolean> cleanUnreachableChildren(NetworkAccess network, SymmetricKey baseKey, FilePointer ourPointer, SigningKeyPair signer) {
CompletableFuture<List<RetrievedFilePointer>> subdirsFuture = network.retrieveAllMetadata(this.subfolders, baseKey);
CompletableFuture<List<RetrievedFilePointer>> filesFuture = network.retrieveAllMetadata(this.files, this.subfolders2files.target(baseKey));
CompletableFuture<List<RetrievedFilePointer>> moreChildrenFuture = moreFolderContents.isPresent() ?
network.retrieveAllMetadata(Arrays.asList(moreFolderContents.get()), baseKey) :
CompletableFuture.completedFuture(Collections.emptyList());
return getChildren(network, baseKey)
.thenCompose(reachable -> subdirsFuture
.thenCompose(subdirs -> filesFuture
.thenCompose(files -> moreChildrenFuture
.thenCompose(moreChildrenSource -> {
// this only has one or zero elements
Optional<RetrievedFilePointer> any = moreChildrenSource.stream().findAny();
CompletableFuture<Boolean> moreChildren = any
.map(d -> ((DirAccess)d.fileAccess)
.cleanUnreachableChildren(network, d.filePointer.baseKey, d.filePointer, signer))
.orElse(CompletableFuture.completedFuture(true));
return moreChildren.thenApply(moreRetrievedChildren -> {
List<SymmetricLocationLink> reachableDirLinks = subfolders
.stream()
.filter(sym -> reachable.stream()
.anyMatch(rfp -> rfp.filePointer.equals(sym.toReadableFilePointer(baseKey))))
.collect(Collectors.toList());
List<SymmetricLocationLink> reachableFileLinks = this.files
.stream()
.filter(sym -> reachable.stream()
.anyMatch(rfp -> rfp.filePointer.equals(sym.toReadableFilePointer(subfolders2files.target(baseKey)))))
.collect(Collectors.toList());
this.subfolders = reachableDirLinks;
this.files = reachableFileLinks;
return network.uploadChunk(this, ourPointer.getLocation(), signer);
});
})
)
)).thenApply(x -> true);
}
public Set<Location> getChildrenLocations(SymmetricKey baseKey) {
SymmetricKey filesKey = this.subfolders2files.target(baseKey);
return Stream.concat(subfolders.stream().map(d -> d.targetLocation(baseKey)),
files.stream().map(f -> f.targetLocation(filesKey)))
.collect(Collectors.toSet());
}
public SymmetricKey getParentKey(SymmetricKey subfoldersKey) {
return this.subfolders2parent.target(subfoldersKey);
}
public SymmetricKey getFilesKey(SymmetricKey subfoldersKey) {
return this.subfolders2files.target(subfoldersKey);
}
// returns pointer to new child directory
public CompletableFuture<FilePointer> mkdir(String name, NetworkAccess network,
PublicSigningKey ownerPublic, SigningKeyPair writer, byte[] ourMapKey,
SymmetricKey baseKey, SymmetricKey optionalBaseKey,
boolean isSystemFolder, SafeRandom random) {
SymmetricKey dirReadKey = optionalBaseKey != null ? optionalBaseKey : SymmetricKey.random();
byte[] dirMapKey = new byte[32]; // root will be stored under this in the btree
random.randombytes(dirMapKey, 0, 32);
SymmetricKey ourParentKey = this.getParentKey(baseKey);
Location ourLocation = new Location(ownerPublic, writer.publicSigningKey, ourMapKey);
DirAccess dir = DirAccess.create(dirReadKey, new FileProperties(name, 0, LocalDateTime.now(),
isSystemFolder, Optional.empty()), ourLocation, ourParentKey, null);
CompletableFuture<FilePointer> result = new CompletableFuture<>();
Location chunkLocation = new Location(ownerPublic, writer.publicSigningKey, dirMapKey);
network.uploadChunk(dir, chunkLocation, writer).thenAccept(success -> {
if (success) {
FilePointer ourPointer = new FilePointer(ownerPublic, writer, ourMapKey, baseKey);
FilePointer subdirPointer = new FilePointer(chunkLocation, Optional.empty(), dirReadKey);
addSubdirAndCommit(subdirPointer, baseKey, ourPointer, writer, network, random)
.thenAccept(modified -> result.complete(new FilePointer(ownerPublic, writer, dirMapKey, dirReadKey)));
} else
result.completeExceptionally(new IllegalStateException("Couldn't upload directory metadata!"));
});
return result;
}
public CompletableFuture<DirAccess> commit(Location ourLocation, SigningKeyPair signer, NetworkAccess network) {
return network.uploadChunk(this, ourLocation, signer)
.thenApply(x -> this);
}
public CompletableFuture<DirAccess> copyTo(SymmetricKey baseKey, SymmetricKey newBaseKey, Location parentLocation,
SymmetricKey parentparentKey,
PublicSigningKey owner, SigningKeyPair entryWriterKey, byte[] newMapKey,
NetworkAccess network, SafeRandom random) {
SymmetricKey parentKey = getParentKey(baseKey);
FileProperties props = getFileProperties(parentKey);
DirAccess da = DirAccess.create(newBaseKey, props, parentLocation, parentparentKey, parentKey);
SymmetricKey ourNewParentKey = da.getParentKey(newBaseKey);
Location ourNewLocation = new Location(owner, entryWriterKey.publicSigningKey, newMapKey);
return this.getChildren(network, baseKey).thenCompose(RFPs -> {
// upload new metadata blob for each child and re-add child
CompletableFuture<DirAccess> reduce = RFPs.stream().reduce(CompletableFuture.completedFuture(da), (dirFuture, rfp) -> {
SymmetricKey newChildBaseKey = rfp.fileAccess.isDirectory() ? SymmetricKey.random() : rfp.filePointer.baseKey;
byte[] newChildMapKey = new byte[32];
random.randombytes(newChildMapKey, 0, 32);
Location newChildLocation = new Location(owner, entryWriterKey.publicSigningKey, newChildMapKey);
return rfp.fileAccess.copyTo(rfp.filePointer.baseKey, newChildBaseKey,
ourNewLocation, ourNewParentKey, entryWriterKey, newChildMapKey, network)
.thenCompose(newChildFileAccess -> {
FilePointer ourNewPointer = new FilePointer(ourNewLocation.owner, entryWriterKey.publicSigningKey, newMapKey, newBaseKey);
FilePointer newChildPointer = new FilePointer(newChildLocation, Optional.empty(), newChildBaseKey);
if (newChildFileAccess.isDirectory())
return dirFuture.thenCompose(dirAccess ->
dirAccess.addSubdirAndCommit(newChildPointer, newBaseKey, ourNewPointer, entryWriterKey, network, random));
else
return dirFuture.thenCompose(dirAccess ->
dirAccess.addFileAndCommit(newChildPointer, newBaseKey, ourNewPointer, entryWriterKey, network, random));
});
}, (a, b) -> a.thenCompose(x -> b)); // TODO Think about this combiner function
return reduce;
}).thenCompose(finalDir -> finalDir.commit(new Location(parentLocation.owner, entryWriterKey.publicSigningKey, newMapKey), entryWriterKey, network));
}
public static DirAccess create(SymmetricKey subfoldersKey, FileProperties metadata, Location parentLocation, SymmetricKey parentParentKey, SymmetricKey parentKey) {
SymmetricKey metaKey = SymmetricKey.random();
if (parentKey == null)
parentKey = SymmetricKey.random();
SymmetricKey filesKey = SymmetricKey.random();
byte[] metaNonce = metaKey.createNonce();
SymmetricLocationLink parentLink = parentLocation == null ? null : SymmetricLocationLink.create(parentKey, parentParentKey, parentLocation);
return new DirAccess(SymmetricLink.fromPair(subfoldersKey, filesKey),
SymmetricLink.fromPair(subfoldersKey, parentKey),
new ArrayList<>(), new ArrayList<>(),
SymmetricLink.fromPair(parentKey, metaKey),
ArrayOps.concat(metaNonce, metaKey.encrypt(metadata.serialize(), metaNonce)),
null,
parentLink,
Optional.empty()
);
}
}