package peergos.shared.user;
import peergos.shared.*;
import peergos.shared.cbor.*;
import peergos.shared.corenode.*;
import peergos.shared.crypto.*;
import peergos.shared.crypto.asymmetric.*;
import peergos.shared.crypto.symmetric.*;
import peergos.shared.io.ipfs.multihash.Multihash;
import peergos.shared.merklebtree.*;
import peergos.shared.user.fs.*;
import peergos.shared.util.*;
import java.io.*;
import java.nio.file.*;
import java.time.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;
import java.util.stream.*;
import jsinterop.annotations.*;
public class UserContext {
private static final boolean LOGGING = false;
public static final String SHARED_DIR_NAME = "shared";
@JsProperty
public final String username;
public final SigningKeyPair signer;
public final BoxingKeyPair boxer;
public final Fragmenter fragmenter;
private CompletableFuture<CommittedWriterData> userData;
@JsProperty
public TrieNode entrie = new TrieNode(); // ba dum che!
// Contact external world
@JsProperty
public final NetworkAccess network;
// In process only
@JsProperty
public final Crypto crypto;
public UserContext(String username, SigningKeyPair signer, BoxingKeyPair boxer, NetworkAccess network, Crypto crypto, CompletableFuture<CommittedWriterData> userData) {
this(username, signer, boxer, network, crypto, Fragmenter.getInstance(), userData);
}
public UserContext(String username, SigningKeyPair signer, BoxingKeyPair boxer, NetworkAccess network,
Crypto crypto, Fragmenter fragmenter, CompletableFuture<CommittedWriterData> userData) {
this.username = username;
this.signer = signer;
this.boxer = boxer;
this.network = network;
this.crypto = crypto;
this.fragmenter = fragmenter;
this.userData = userData;
}
public boolean isJavascript() {
return this.network.isJavascript();
}
@JsMethod
public static CompletableFuture<UserContext> signIn(String username, String password, NetworkAccess network, Crypto crypto) {
return getWriterDataCbor(network, username)
.thenCompose(pair -> {
Optional<UserGenerationAlgorithm> algorithmOpt = WriterData.extractUserGenerationAlgorithm(pair.right);
if (!algorithmOpt.isPresent())
throw new IllegalStateException("No login algorithm specified in user data!");
UserGenerationAlgorithm algorithm = algorithmOpt.get();
return UserUtil.generateUser(username, password, crypto.hasher, crypto.symmetricProvider,
crypto.random, crypto.signer, crypto.boxer, algorithm)
.thenCompose(userWithRoot -> {
try {
WriterData userData = WriterData.fromCbor(pair.right, userWithRoot.getRoot());
return createOurFileTreeOnly(username, userData, network)
.thenCompose(root -> TofuCoreNode.load(username, root, network, crypto.random)
.thenCompose(keystore -> {
TofuCoreNode tofu = new TofuCoreNode(network.coreNode, keystore);
UserContext result = new UserContext(username, userWithRoot.getUser(),
userWithRoot.getBoxingPair(), network.withCorenode(tofu), crypto,
CompletableFuture.completedFuture(new CommittedWriterData(MaybeMultihash.of(pair.left), userData)));
tofu.setContext(result);
System.out.println("Initializing context..");
return result.init();
}));
} catch (Throwable t) {
throw new IllegalStateException("Incorrect password");
}
});
}).exceptionally(Futures::logError);
}
@JsMethod
public static CompletableFuture<UserContext> signUp(String username, String password, NetworkAccess network, Crypto crypto) {
return signUpGeneral(username, password, network, crypto, UserGenerationAlgorithm.getDefault());
}
public static CompletableFuture<UserContext> signUpGeneral(String username, String password, NetworkAccess network, Crypto crypto, UserGenerationAlgorithm algorithm) {
return UserUtil.generateUser(username, password, crypto.hasher, crypto.symmetricProvider, crypto.random, crypto.signer, crypto.boxer, algorithm)
.thenCompose(userWithRoot -> {
WriterData newUserData = WriterData.createEmpty(Optional.of(userWithRoot.getBoxingPair().publicBoxingKey), userWithRoot.getRoot());
CommittedWriterData notCommitted = new CommittedWriterData(MaybeMultihash.EMPTY(), newUserData);
UserContext context = new UserContext(username, userWithRoot.getUser(), userWithRoot.getBoxingPair(),
network, crypto, CompletableFuture.completedFuture(notCommitted));
System.out.println("Registering username " + username);
return context.register().thenCompose(successfullyRegistered -> {
if (!successfullyRegistered) {
System.out.println("Couldn't register username");
throw new IllegalStateException("Couldn't register username: " + username);
}
System.out.println("Creating user's root directory");
long t1 = System.currentTimeMillis();
return context.createEntryDirectory(username).thenCompose(userRoot -> {
System.out.println("Creating root directory took " + (System.currentTimeMillis() - t1) + " mS");
return ((DirAccess) userRoot.fileAccess).mkdir(SHARED_DIR_NAME, network,
userRoot.filePointer.getLocation().owner,
userRoot.filePointer.signer(),
userRoot.filePointer.location.getMapKey(),
userRoot.filePointer.baseKey, null, true, crypto.random)
.thenCompose(x -> signIn(username, password, network.clear(), crypto));
});
});
}).exceptionally(Futures::logError);
}
@JsMethod
public static CompletableFuture<UserContext> fromPublicLink(String link, NetworkAccess network, Crypto crypto) {
FilePointer entryPoint = FilePointer.fromLink(link);
EntryPoint entry = new EntryPoint(entryPoint, "", Collections.emptySet(), Collections.emptySet());
CommittedWriterData committed = new CommittedWriterData(MaybeMultihash.EMPTY(), WriterData.createEmpty(Optional.empty(), null));
CompletableFuture<CommittedWriterData> userData = CompletableFuture.completedFuture(committed);
UserContext context = new UserContext(null, null, null, network.clear(), crypto, userData);
return context.addEntryPoint(null, context.entrie, entry, network).thenApply(trieNode -> {
context.entrie = trieNode;
return context;
});
}
@JsMethod
public CompletableFuture<String> getEntryPath() {
if (username != null)
return CompletableFuture.completedFuture("/");
CompletableFuture<Optional<FileTreeNode>> dir = getByPath("/");
return dir.thenCompose(opt -> getLinkPath(opt.get()))
.thenApply(path -> path.substring(1)); // strip off extra slash at root
}
private CompletableFuture<String> getLinkPath(FileTreeNode file) {
if (! file.isDirectory())
return CompletableFuture.completedFuture("");
return file.getChildren(network)
.thenCompose(children -> {
if (children.size() != 1)
return CompletableFuture.completedFuture(file.getName());
FileTreeNode child = children.stream().findAny().get();
if (child.isReadable()) // case where a directory was shared with exactly one direct child
return CompletableFuture.completedFuture(file.getName() + "/" + child.getName());
return getLinkPath(child)
.thenApply(p -> file.getName() + (p.length() > 0 ? "/" + p : ""));
});
}
public static CompletableFuture<UserContext> ensureSignedUp(String username, String password, NetworkAccess network, Crypto crypto) {
return network.isUsernameRegistered(username).thenCompose(isRegistered -> {
if (isRegistered)
return signIn(username, password, network, crypto);
return signUp(username, password, network, crypto);
});
}
private CompletableFuture<UserContext> init() {
CompletableFuture<CommittedWriterData> lock = new CompletableFuture<>();
return addToUserDataQueue(lock)
.thenCompose(wd -> createFileTree(username, wd.props, network)
.thenCompose(root -> {
this.entrie = root;
return getByPath("/" + username + "/" + "shared")
.thenApply(sharedOpt -> {
if (!sharedOpt.isPresent())
throw new IllegalStateException("Couldn't find shared folder!");
lock.complete(wd);
return this;
});
}));
}
public CompletableFuture<Boolean> cleanEntryPoints() {
CompletableFuture<CommittedWriterData> lock = new CompletableFuture<>();
return addToUserDataQueue(lock)
.thenCompose(wd -> Futures.reduceAll(
wd.props.staticData.get().getEntryPoints(),
true,
(t, e) -> cleanOurEntryPoint(e),
(a, b) -> a && b));
}
public CompletableFuture<FileTreeNode> getSharingFolder() {
return getByPath("/"+username + "/shared").thenApply(opt -> opt.get());
}
@JsMethod
public CompletableFuture<Boolean> isRegistered() {
System.out.println("isRegistered");
return network.coreNode.getUsername(signer.publicSigningKey).thenApply(registeredUsername -> {
System.out.println("got username \"" + registeredUsername + "\"");
return this.username.equals(registeredUsername);
});
}
@JsMethod
public CompletableFuture<Boolean> isAvailable() {
return network.coreNode.getPublicKey(username)
.thenApply(publicKey -> !publicKey.isPresent());
}
@JsMethod
public CompletableFuture<Boolean> register() {
return isRegistered().thenCompose(exists -> {
if (exists)
throw new IllegalStateException("Account already exists with username: " + username);
LocalDate now = LocalDate.now();
// set claim expiry to two months from now
LocalDate expiry = now.plusMonths(2);
System.out.println("claiming username: " + this.username + " with expiry " + expiry);
List<UserPublicKeyLink> claimChain = UserPublicKeyLink.createInitial(signer, this.username, expiry);
return network.coreNode.updateChain(this.username, claimChain);
});
}
@JsMethod
public CompletableFuture<Pair<Integer, Integer>> getTotalSpaceUsedJS(PublicSigningKey owner) {
return getTotalSpaceUsed(owner).thenApply(size -> new Pair<>((int)(size >> 32), size.intValue()));
}
public CompletableFuture<Long> getTotalSpaceUsed(PublicSigningKey owner) {
// assume no cycles in owned keys
return getWriterData(network, owner).thenCompose(cwd -> {
CompletableFuture<Long> subtree = Futures.reduceAll(cwd.props.ownedKeys
.stream()
.map(writer -> getTotalSpaceUsed(writer))
.collect(Collectors.toList()),
0L, (t, fut) -> fut.thenApply(x -> x + t), (a, b) -> a + b);
return subtree.thenCompose(ownedSize -> getRecursiveBlockSize(cwd.hash.get())
.thenApply(descendentSize -> descendentSize + ownedSize));
});
}
private CompletableFuture<Long> getRecursiveBlockSize(Multihash block) {
return network.dhtClient.getLinks(block).thenCompose(links -> {
List<CompletableFuture<Long>> subtrees = links.stream().map(this::getRecursiveBlockSize).collect(Collectors.toList());
return network.dhtClient.getSize(block)
.thenCompose(sizeOpt -> {
CompletableFuture<Long> reduced = Futures.reduceAll(subtrees,
0L, (t, fut) -> fut.thenApply(x -> x + t), (a, b) -> a + b);
return reduced.thenApply(sum -> sum + sizeOpt.orElse(0));
});
});
}
@JsMethod
public CompletableFuture<UserContext> changePassword(String oldPassword, String newPassword) {
return getWriterDataCbor(this.network, this.username)
.thenCompose(pair -> {
Optional<UserGenerationAlgorithm> algorithmOpt = WriterData.extractUserGenerationAlgorithm(pair.right);
if (! algorithmOpt.isPresent())
throw new IllegalStateException("No login algorithm specified in user data!");
UserGenerationAlgorithm algorithm = algorithmOpt.get();
return changePassword(oldPassword, newPassword, algorithm, algorithm);
});
}
public CompletableFuture<UserContext> changePassword(String oldPassword, String newPassword,
UserGenerationAlgorithm existingAlgorithm,
UserGenerationAlgorithm newAlgorithm) {
System.out.println("changing password");
LocalDate expiry = LocalDate.now();
// set claim expiry to two months from now
expiry.plusMonths(2);
return UserUtil.generateUser(username, oldPassword, crypto.hasher, crypto.symmetricProvider, crypto.random, crypto.signer, crypto.boxer, existingAlgorithm)
.thenCompose(existingUser -> {
if (!existingUser.getUser().equals(this.signer))
throw new IllegalArgumentException("Incorrect existing password during change password attempt!");
return UserUtil.generateUser(username, newPassword, crypto.hasher, crypto.symmetricProvider,
crypto.random, crypto.signer, crypto.boxer, newAlgorithm)
.thenCompose(updatedUser ->{
CompletableFuture<CommittedWriterData> lock = new CompletableFuture<>();
return addToUserDataQueue(lock).thenCompose(wd -> wd.props
.changeKeys(updatedUser.getUser(), wd.hash, updatedUser.getBoxingPair().publicBoxingKey,
updatedUser.getRoot(), network, lock::complete)
.thenCompose(userData -> {
List<UserPublicKeyLink> claimChain = UserPublicKeyLink.createChain(signer, updatedUser.getUser(), username, expiry);
return network.coreNode.updateChain(username, claimChain).thenCompose(updatedChain -> {
if (!updatedChain)
throw new IllegalStateException("Couldn't register new public keys during password change!");
return UserContext.ensureSignedUp(username, newPassword, network, crypto);
});
})
);
});
});
}
public CompletableFuture<RetrievedFilePointer> createEntryDirectory(String directoryName) {
long t1 = System.currentTimeMillis();
SigningKeyPair writer = SigningKeyPair.random(crypto.random, crypto.signer);
System.out.println("Random User generation took " + (System.currentTimeMillis()-t1) + " mS");
byte[] rootMapKey = new byte[32]; // root will be stored under this in the core node
crypto.random.randombytes(rootMapKey, 0, 32);
SymmetricKey rootRKey = SymmetricKey.random();
System.out.println("Random keys generation took " + (System.currentTimeMillis()-t1) + " mS");
// and authorise the writer key
FilePointer rootPointer = new FilePointer(this.signer.publicSigningKey, writer, rootMapKey, rootRKey);
EntryPoint entry = new EntryPoint(rootPointer, this.username, Collections.emptySet(), Collections.emptySet());
long t2 = System.currentTimeMillis();
DirAccess root = DirAccess.create(rootRKey, new FileProperties(directoryName, 0, LocalDateTime.now(), false, Optional.empty()), (Location)null, null, null);
Location rootLocation = new Location(this.signer.publicSigningKey, writer.publicSigningKey, rootMapKey);
System.out.println("Uploading entry point directory");
return network.uploadChunk(root, rootLocation, writer).thenCompose(uploaded -> {
if (!uploaded)
throw new IllegalStateException("Failed to upload root dir!");
long t3 = System.currentTimeMillis();
System.out.println("Uploading root dir metadata took " + (t3 - t2) + " mS");
return addToStaticDataAndCommit(entry)
.thenCompose(x -> addOwnedKeyAndCommit(entry.pointer.location.writer))
.thenApply(x -> {
System.out.println("Committing static data took " + (System.currentTimeMillis() - t3) + " mS");
if (uploaded)
return new RetrievedFilePointer(rootPointer, root);
throw new IllegalStateException("Failed to create entry directory!");
});
});
}
public CompletableFuture<Optional<Pair<PublicSigningKey, PublicBoxingKey>>> getPublicKeys(String username) {
return network.coreNode.getPublicKey(username)
.thenCompose(signerOpt -> getWriterData(network, signerOpt.get())
.thenApply(wd -> Optional.of(new Pair<>(signerOpt.get(), wd.props.followRequestReceiver.get()))));
}
private synchronized CompletableFuture<CommittedWriterData> addToUserDataQueue(CompletableFuture<CommittedWriterData> replacement) {
CompletableFuture<CommittedWriterData> existing = this.userData;
this.userData = replacement;
return existing;
}
private CompletableFuture<CommittedWriterData> addOwnedKeyAndCommit(PublicSigningKey owned) {
CompletableFuture<CommittedWriterData> lock = new CompletableFuture<>();
return addToUserDataQueue(lock).thenCompose(wd -> {
Set<PublicSigningKey> updated = Stream.concat(wd.props.ownedKeys.stream(), Stream.of(owned))
.collect(Collectors.toSet());
WriterData writerData = wd.props.withOwnedKeys(updated);
return writerData.commit(signer, wd.hash, network, lock::complete);
});
}
@JsMethod
public CompletableFuture<Set<FileTreeNode>> getFriendRoots() {
List<CompletableFuture<Optional<FileTreeNode>>> friendRoots = entrie.getChildNames()
.stream()
.filter(p -> !p.startsWith(username))
.map(p -> getByPath(p)).collect(Collectors.toList());
return Futures.combineAll(friendRoots)
.thenApply(set -> set.stream().filter(opt -> opt.isPresent()).map(opt -> opt.get()).collect(Collectors.toSet()));
}
@JsMethod
public CompletableFuture<Set<String>> getFollowing() {
return getFriendRoots()
.thenApply(set -> set.stream()
.map(froot -> froot.getOwner())
.filter(name -> !name.equals(username))
.collect(Collectors.toSet()));
}
@JsMethod
public CompletableFuture<Set<String>> getFollowerNames() {
return getFollowerRoots().thenApply(m -> m.keySet());
}
public CompletableFuture<Map<String, FileTreeNode>> getFollowerRoots() {
return getSharingFolder()
.thenCompose(sharing -> sharing.getChildren(network))
.thenApply(children -> children.stream()
.collect(Collectors.toMap(e -> e.getFileProperties().name, e -> e)));
}
private CompletableFuture<Set<String>> getFollowers() {
CompletableFuture<CommittedWriterData> lock = new CompletableFuture<>();
return addToUserDataQueue(lock).thenApply(wd -> {
lock.complete(wd);
return wd.props.staticData.get()
.getEntryPoints()
.stream()
.map(e -> e.owner)
.filter(name -> ! name.equals(username))
.collect(Collectors.toSet());
});
}
@JsMethod
public CompletableFuture<SocialState> getSocialState() {
return processFollowRequests()
.thenCompose(pending -> getFollowerRoots()
.thenCompose(followerRoots -> getFriendRoots()
.thenCompose(followingRoots -> getFollowers().thenApply(followers -> new SocialState(pending, followers, followerRoots, followingRoots)))));
}
@JsMethod
public CompletableFuture<Boolean> sendInitialFollowRequest(String targetUsername) {
if(username.equals(targetUsername)) {
return CompletableFuture.completedFuture(false);
}
return sendFollowRequest(targetUsername, SymmetricKey.random());
}
@JsMethod
public CompletableFuture<Boolean> sendReplyFollowRequest(FollowRequest initialRequest, boolean accept, boolean reciprocate) {
String theirUsername = initialRequest.entry.get().owner;
// if accept, create directory to share with them, note in entry points (they follow us)
if (!accept && !reciprocate) {
// send a null entry and null key (full rejection)
DataSink dout = new DataSink();
// write a null entry point
EntryPoint entry = new EntryPoint(FilePointer.createNull(), username, Collections.emptySet(), Collections.emptySet());
dout.writeArray(entry.serialize());
dout.writeArray(new byte[0]); // tell them we're not reciprocating
byte[] plaintext = dout.toByteArray();
return getPublicKeys(initialRequest.entry.get().owner).thenCompose(pair -> {
PublicBoxingKey targetUser = pair.get().right;
// create a tmp keypair whose public key we can prepend to the request without leaking information
BoxingKeyPair tmp = BoxingKeyPair.random(crypto.random, crypto.boxer);
byte[] payload = targetUser.encryptMessageFor(plaintext, tmp.secretBoxingKey);
DataSink resp = new DataSink();
resp.writeArray(tmp.publicBoxingKey.serialize());
resp.writeArray(payload);
network.coreNode.followRequest(initialRequest.entry.get().pointer.location.owner, resp.toByteArray());
// remove pending follow request from them
return network.coreNode.removeFollowRequest(signer.publicSigningKey, signer.signMessage(initialRequest.rawCipher));
});
}
return CompletableFuture.completedFuture(true).thenCompose(b -> {
DataSink dout = new DataSink();
if (accept) {
return getSharingFolder().thenCompose(sharing -> {
return sharing.mkdir(theirUsername, network, initialRequest.key.get(), true, crypto.random)
.thenCompose(friendRoot -> {
// add a note to our static data so we know who we sent the read access to
EntryPoint entry = new EntryPoint(friendRoot.readOnly(), username, Collections.singleton(theirUsername), Collections.emptySet());
return addToStaticDataAndCommit(entry).thenApply(trie -> {
this.entrie = trie;
dout.writeArray(entry.serialize());
return dout;
});
});
});
} else {
EntryPoint entry = new EntryPoint(FilePointer.createNull(), username, Collections.emptySet(), Collections.emptySet());
dout.writeArray(entry.serialize());
return CompletableFuture.completedFuture(dout);
}
}).thenCompose(dout -> {
if (!reciprocate) {
dout.writeArray(new byte[0]); // tell them we're not reciprocating
} else {
// if reciprocate, add entry point to their shared directory (we follow them) and then
dout.writeArray(initialRequest.entry.get().pointer.baseKey.serialize()); // tell them we are reciprocating
}
byte[] plaintext = dout.toByteArray();
return getPublicKeys(initialRequest.entry.get().owner).thenCompose(pair -> {
PublicBoxingKey targetUser = pair.get().right;
// create a tmp keypair whose public key we can prepend to the request without leaking information
BoxingKeyPair tmp = BoxingKeyPair.random(crypto.random, crypto.boxer);
byte[] payload = targetUser.encryptMessageFor(plaintext, tmp.secretBoxingKey);
DataSink resp = new DataSink();
resp.writeArray(tmp.publicBoxingKey.serialize());
resp.writeArray(payload);
return network.coreNode.followRequest(initialRequest.entry.get().pointer.location.owner, resp.toByteArray());
});
}).thenCompose(b -> {
if (reciprocate)
return addToStaticDataAndCommit(initialRequest.entry.get());
return CompletableFuture.completedFuture(entrie);
}).thenCompose(trie -> {
// remove original request
entrie = trie;
return network.coreNode.removeFollowRequest(signer.publicSigningKey, signer.signMessage(initialRequest.rawCipher));
});
}
public CompletableFuture<Boolean> sendFollowRequest(String targetUsername, SymmetricKey requestedKey) {
return getSharingFolder().thenCompose(sharing -> {
return sharing.getChildren(network).thenCompose(children -> {
boolean alreadySentRequest = children.stream()
.filter(f -> f.getFileProperties().name.equals(targetUsername))
.findAny()
.isPresent();
if (alreadySentRequest)
return CompletableFuture.completedFuture(false);
// check for them not reciprocating
return getFollowing().thenCompose(following -> {
boolean alreadyFollowing = following.stream().filter(x -> x.equals(targetUsername)).findAny().isPresent();
if (alreadyFollowing)
return CompletableFuture.completedFuture(false);
return getPublicKeys(targetUsername).thenCompose(targetUserOpt -> {
if (!targetUserOpt.isPresent())
return CompletableFuture.completedFuture(false);
PublicBoxingKey targetUser = targetUserOpt.get().right;
return sharing.mkdir(targetUsername, network, null, true, crypto.random).thenCompose(friendRoot -> {
// if they accept the request we will add a note to our static data so we know who we sent the read access to
EntryPoint entry = new EntryPoint(friendRoot.readOnly(), username, Collections.singleton(targetUsername), Collections.emptySet());
// send details to allow friend to follow us, and optionally let us follow them
// create a tmp keypair whose public key we can prepend to the request without leaking information
BoxingKeyPair tmp = BoxingKeyPair.random(crypto.random, crypto.boxer);
DataSink buf = new DataSink();
buf.writeArray(entry.serialize());
buf.writeArray(requestedKey != null ? requestedKey.serialize() : new byte[0]);
byte[] plaintext = buf.toByteArray();
byte[] payload = targetUser.encryptMessageFor(plaintext, tmp.secretBoxingKey);
DataSink res = new DataSink();
res.writeArray(tmp.publicBoxingKey.serialize());
res.writeArray(payload);
PublicSigningKey targetSigner = targetUserOpt.get().left;
return network.coreNode.followRequest(targetSigner, res.toByteArray());
});
});
});
});
});
};
public CompletableFuture<Boolean> sendWriteAccess(PublicSigningKey targetUser) {
/*
// create sharing keypair and give it write access
User sharing = User.random(random, signer, boxer);
byte[] rootMapKey = new byte[32];
random.randombytes(rootMapKey, 0, 32);
// add a note to our static data so we know who we sent the private key to
FilePointer friendRoot = new FilePointer(user, sharing, rootMapKey, SymmetricKey.random());
return corenodeClient.getUsername(targetUser).thenCompose(name -> {
EntryPoint entry = new EntryPoint(friendRoot, username, Collections.emptySet(), Stream.of(name).collect(Collectors.toSet()));
try {
addToStaticDataAndCommit(entry);
// create a tmp keypair whose public key we can append to the request without leaking information
User tmp = User.random(random, signer, boxer);
byte[] payload = entry.serializeAndEncrypt(tmp, targetUser);
return corenodeClient.followRequest(targetUser, ArrayOps.concat(tmp.publicBoxingKey.toByteArray(), payload));
} catch (IOException e) {
throw new RuntimeException(e);
}
});*/
throw new IllegalStateException("Unimplemented!");
}
public CompletableFuture<Boolean> unShare(Path path, String readerToRemove) {
return unShare(path, Collections.singleton(readerToRemove));
}
public CompletableFuture<Boolean> unShare(Path path, Set<String> readersToRemove) {
String pathString = path.toString();
CompletableFuture<Optional<FileTreeNode>> byPath = getByPath(pathString);
return byPath.thenCompose(opt -> {
//
// first remove links from shared directory
//
FileTreeNode sharedPath = opt.orElseThrow(() -> new IllegalStateException("Specified un-shareWith path " + pathString + " does not exist"));
Optional<String> empty = Optional.empty();
Function<String, CompletableFuture<Optional<String>>> unshareWith = user -> getByPath("/" + username + "/shared/" + user)
.thenCompose(sharedWithOpt -> {
if (!sharedWithOpt.isPresent())
return CompletableFuture.completedFuture(empty);
FileTreeNode sharedRoot = sharedWithOpt.get();
return sharedRoot.removeChild(sharedPath, network)
.thenCompose(x -> CompletableFuture.completedFuture(Optional.of(user)));
});
return sharedWith(sharedPath)
.thenCompose(sharedWithUsers -> {
Set<CompletableFuture<Optional<String>>> collect = sharedWithUsers.stream()
.map(unshareWith::apply) //remove link from shared directory
.collect(Collectors.toSet());
return Futures.combineAll(collect);
}).thenCompose(x -> {
List<String> allSharees = x.stream()
.flatMap(e -> e.isPresent() ? Stream.of(e.get()) : Stream.empty())
.collect(Collectors.toList());
Set<String> remainingReaders = allSharees.stream()
.filter(reader -> ! readersToRemove.contains(reader))
.collect(Collectors.toSet());
return shareWith(path, remainingReaders);
});
});
/*
Optional<FileTreeNode> f = getByPath(path.toString());
if (! f.isPresent())
return;
FileTreeNode file = f.get();
Set<String> sharees = sharedWith(file);
// first remove links from shared directory
for (String friendName: sharees) {
Optional<FileTreeNode> opt = getByPath("/" + username + "/shared/" + friendName);
if (!opt.isPresent())
continue;
FileTreeNode sharedRoot = opt.get();
sharedRoot.removeChild(file, this);
}
// now change to new base keys, clean some keys and mark others as dirty
FileTreeNode parent = getByPath(path.getParent().toString()).get();
file.makeDirty(this, parent, readersToRemove);
// now re-share new keys with remaining users
Set<String> remainingReaders = sharees.stream().filter(name -> !readersToRemove.contains(name)).collect(Collectors.toSet());
share(path, remainingReaders);*/
}
@JsMethod
public CompletableFuture<Set<String>> sharedWith(FileTreeNode file) {
Location fileLocation = file.getLocation();
String path = "/" + username + "/shared";
Function<FileTreeNode, CompletableFuture<Optional<String>>> func = sharedUserDir -> {
CompletableFuture<Set<FileTreeNode>> children = sharedUserDir.getChildren(network);
return children.thenCompose(e -> {
boolean present = e.stream()
.filter(sharedFile -> sharedFile.getLocation().equals(fileLocation))
.findFirst()
.isPresent();
String userName = present ? sharedUserDir.getFileProperties().name : null;
return CompletableFuture.completedFuture(Optional.ofNullable(userName));
});
};
return getByPath(path)
.thenCompose(sharedDirOpt -> {
FileTreeNode sharedDir = sharedDirOpt.orElseThrow(() -> new IllegalStateException("No such directory" + path));
return sharedDir.getChildren(network)
.thenCompose(sharedUserDirs -> {
List<CompletableFuture<Optional<String>>> collect = sharedUserDirs.stream()
.map(func::apply)
.collect(Collectors.toList());
return Futures.combineAll(collect);
}).thenCompose(optSet -> {
Set<String> sharedWith = optSet.stream()
.flatMap(e -> e.isPresent() ? Stream.of(e.get()) : Stream.empty())
.collect(Collectors.toSet());
return CompletableFuture.completedFuture(sharedWith);
});
});
/*
FileTreeNode sharedDir = getByPath("/" + username + "/shared").get();
Set<FileTreeNode> friendDirs = sharedDir.getChildren(this);
return friendDirs.stream()
.filter(friendDir -> friendDir.getChildren(this)
.stream()
.filter(f -> f.getLocation().equals(file.getLocation()))
.findAny()
.isPresent())
.map(u -> u.getFileProperties().name)
.collect(Collectors.toSet());*/
// throw new IllegalStateException("Unimplemented!");
}
public CompletableFuture<Boolean> shareWith(Path path, Set<String> readersToAdd) {
return getByPath(path.toString())
.thenCompose(file -> shareWithAll(file.orElseThrow(() -> new IllegalStateException("Could not find path " + path.toString())), readersToAdd));
}
public CompletableFuture<Boolean> shareWithAll(FileTreeNode file, Set<String> readersToAdd) {
return Futures.reduceAll(readersToAdd,
true,
(x, username) -> shareWith(file, username),
(a, b) -> a && b);
}
@JsMethod
public CompletableFuture<Boolean> shareWith(FileTreeNode file, String usernameToGrantReadAccess) {
return getByPath("/" + username + "/shared/" + usernameToGrantReadAccess)
.thenCompose(shared -> {
if (!shared.isPresent())
return CompletableFuture.completedFuture(true);
FileTreeNode sharedTreeNode = shared.get();
return sharedTreeNode.addLinkTo(file, network, crypto.random)
.thenCompose(ee -> CompletableFuture.completedFuture(true));
});
}
private CompletableFuture<TrieNode> addToStaticDataAndCommit(EntryPoint entry) {
return addToStaticDataAndCommit(entrie, entry);
}
private synchronized CompletableFuture<TrieNode> addToStaticDataAndCommit(TrieNode root, EntryPoint entry) {
CompletableFuture<CommittedWriterData> lock = new CompletableFuture<>();
return addToUserDataQueue(lock).thenCompose(wd -> {
wd.props.staticData.ifPresent(sd -> sd.add(entry));
return wd.props.commit(signer, wd.hash, network, lock::complete)
.thenCompose(res -> addEntryPoint(username, root, entry, network))
.exceptionally(t -> {
lock.complete(wd);
return root;
});
});
}
private CompletableFuture<CommittedWriterData> removeFromStaticData(FileTreeNode fileTreeNode) {
CompletableFuture<CommittedWriterData> lock = new CompletableFuture<>();
return addToUserDataQueue(lock)
.thenCompose(wd -> wd.props.removeFromStaticData(fileTreeNode, signer, wd.hash, network, lock::complete));
}
/**
* Process any responses to our follow requests.
*
* @return initial follow requests
*/
public CompletableFuture<List<FollowRequest>> processFollowRequests() {
return network.coreNode.getFollowRequests(signer.publicSigningKey).thenCompose(reqs -> {
DataSource din = new DataSource(reqs);
List<FollowRequest> all;
try {
int n = din.readInt();
all = IntStream.range(0, n)
.mapToObj(i -> i)
.flatMap(i -> {
try {
return Stream.of(decodeFollowRequest(din.readArray()));
} catch (IOException ioe) {
return Stream.empty();
}
})
.collect(Collectors.toList());
} catch (IOException e) {
throw new RuntimeException(e);
}
return processFollowRequests(all);
});
}
private CompletableFuture<List<FollowRequest>> processFollowRequests(List<FollowRequest> all) {
return getSharingFolder().thenCompose(sharing ->
getFollowerRoots().thenCompose(followerRoots -> {
List<FollowRequest> replies = all.stream()
.filter(freq -> followerRoots.containsKey(freq.entry.get().owner))
.collect(Collectors.toList());
BiFunction<TrieNode, FollowRequest, CompletableFuture<TrieNode>> addToStatic = (root, freq) -> {
if (!Arrays.equals(freq.entry.get().pointer.baseKey.serialize(), SymmetricKey.createNull().serialize())) {
CompletableFuture<TrieNode> updatedRoot = freq.entry.get().owner.equals(username) ?
CompletableFuture.completedFuture(root) : // ignore responses claiming to be owned by us
addToStaticDataAndCommit(root, freq.entry.get());
return updatedRoot.thenCompose(newRoot -> {
entrie = newRoot;
// clear their response follow req too
return network.coreNode.removeFollowRequest(signer.publicSigningKey, signer.signMessage(freq.rawCipher))
.thenApply(b -> newRoot);
});
}
return CompletableFuture.completedFuture(root);
};
BiFunction<TrieNode, FollowRequest, CompletableFuture<TrieNode>> mozart = (trie, freq) -> {
// delete our folder if they didn't reciprocate
FileTreeNode ourDirForThem = followerRoots.get(freq.entry.get().owner);
byte[] ourKeyForThem = ourDirForThem.getKey().serialize();
byte[] keyFromResponse = freq.key.map(k -> k.serialize()).orElse(null);
if (keyFromResponse == null || !Arrays.equals(keyFromResponse, ourKeyForThem)) {
// They didn't reciprocate (follow us)
CompletableFuture<Boolean> removeDir = ourDirForThem.remove(network, sharing);
// remove entry point as well
CompletableFuture<CommittedWriterData> cleanStatic = removeFromStaticData(ourDirForThem);
return removeDir.thenCompose(x -> cleanStatic)
.thenCompose(b -> addToStatic.apply(trie, freq));
} else if (freq.entry.get().pointer.isNull()) {
// They reciprocated, but didn't accept (they follow us, but we can't follow them)
// add entry point to static data to signify their acceptance
EntryPoint entryWeSentToThem = new EntryPoint(ourDirForThem.getPointer().filePointer.readOnly(),
username, Collections.singleton(ourDirForThem.getName()), Collections.emptySet());
return addToStaticDataAndCommit(trie, entryWeSentToThem);
} else {
// they accepted and reciprocated
// add entry point to static data to signify their acceptance
EntryPoint entryWeSentToThem = new EntryPoint(ourDirForThem.getPointer().filePointer.readOnly(),
username, Collections.singleton(ourDirForThem.getName()), Collections.emptySet());
// add new entry point to tree root
EntryPoint entry = freq.entry.get();
if (entry.owner.equals(username))
throw new IllegalStateException("Received a follow request claiming to be owner by us!");
return addToStaticDataAndCommit(trie, entryWeSentToThem)
.thenCompose(newRoot -> network.retrieveEntryPoint(entry).thenCompose(treeNode ->
treeNode.get().getPath(network)).thenApply(path ->
newRoot.put(path, entry)
).thenCompose(trieres -> addToStatic.apply(trieres, freq).thenApply(b -> trieres)));
}
};
List<FollowRequest> initialRequests = all.stream()
.filter(freq -> !followerRoots.containsKey(freq.entry.get().owner))
.collect(Collectors.toList());
return Futures.reduceAll(replies, entrie, mozart, (a, b) -> a)
.thenApply(newRoot -> {
entrie = newRoot;
return initialRequests;
});
})
);
}
private FollowRequest decodeFollowRequest(byte[] raw) throws IOException {
DataSource buf = new DataSource(raw);
PublicBoxingKey tmp = PublicBoxingKey.fromCbor(CborObject.fromByteArray(Serialize.deserializeByteArray(buf, 4096)));
byte[] cipher = buf.readArray();
byte[] plaintext = boxer.secretBoxingKey.decryptMessage(cipher, tmp);
DataSource input = new DataSource(plaintext);
byte[] rawEntry = input.readArray();
byte[] rawKey = input.readArray();
return new FollowRequest(rawEntry.length > 0 ? Optional.of(EntryPoint.fromCbor(CborObject.fromByteArray(rawEntry))) : Optional.empty(),
rawKey.length > 0 ? Optional.of(SymmetricKey.fromByteArray(rawKey)) : Optional.empty(), raw);
}
public CompletableFuture<Set<FileTreeNode>> getChildren(String path) {
return entrie.getChildren(path, network);
}
@JsMethod
public CompletableFuture<Optional<FileTreeNode>> getByPath(String path) {
if (path.equals("/"))
return CompletableFuture.completedFuture(Optional.of(FileTreeNode.createRoot(entrie)));
return entrie.getByPath(path, network);
}
public CompletableFuture<FileTreeNode> getUserRoot() {
return getByPath("/"+username).thenApply(opt -> opt.get());
}
/**
*
* @return TrieNode for root of filesystem containing only our files
*/
private static CompletableFuture<TrieNode> createOurFileTreeOnly(String ourName, WriterData userData, NetworkAccess network) {
TrieNode root = new TrieNode();
if (! userData.staticData.isPresent())
throw new IllegalStateException("Cannot retrieve file tree for a filesystem without entrypoints!");
List<EntryPoint> ourFileSystemEntries = userData.staticData.get()
.getEntryPoints()
.stream()
.filter(e -> e.owner.equals(ourName))
.collect(Collectors.toList());
return Futures.reduceAll(ourFileSystemEntries, root, (t, e) -> addEntryPoint(ourName, t, e, network), (a, b) -> a)
.exceptionally(Futures::logError);
}
/**
*
* @return TrieNode for root of filesystem
*/
private static CompletableFuture<TrieNode> createFileTree(String ourName, WriterData userData, NetworkAccess network) {
TrieNode root = new TrieNode();
if (! userData.staticData.isPresent())
throw new IllegalStateException("Cannot retrieve file tree for a filesystem without entrypoints!");
return Futures.reduceAll(userData.staticData.get().getEntryPoints(), root, (t, e) -> addEntryPoint(ourName, t, e, network), (a, b) -> a)
.exceptionally(Futures::logError);
}
private static CompletableFuture<TrieNode> addEntryPoint(String ourName, TrieNode root, EntryPoint e, NetworkAccess network) {
return network.retrieveEntryPoint(e).thenCompose(metadata -> {
if (metadata.isPresent()) {
return metadata.get().getPath(network)
.thenCompose(path -> {
// check entrypoint doesn't forge the owner
return (e.owner.equals(ourName) ? CompletableFuture.completedFuture(true) :
e.isValid(path, network)).thenApply(valid -> {
System.out.println("Added entry point: " + metadata.get() + " at path " + path);
String[] parts = path.split("/");
if (parts.length < 3 || !parts[2].equals(SHARED_DIR_NAME))
return root.put(path, e);
TrieNode rootWithMapping = parts[1].equals(ourName) ? root : root.addPathMapping("/" + parts[1] + "/", path + "/");
return rootWithMapping.put(path, e);
});
}).exceptionally(t -> {
System.err.println("Couldn't add entry point (failed retrieving parent dir or it was invalid): " + metadata.get().getName());
// Allow the system to continue without this entry point
return root;
});
}
throw new IllegalStateException("Metadata blob not Present downloading entry point!");
}).exceptionally(Futures::logError);
}
private CompletableFuture<Boolean> cleanOurEntryPoint(EntryPoint e) {
if (! e.owner.equals(username))
return CompletableFuture.completedFuture(false);
return network.retrieveEntryPoint(e).thenCompose(fileOpt -> {
if (! fileOpt.isPresent())
return CompletableFuture.completedFuture(true);
FileTreeNode file = fileOpt.get();
return file.getPath(network)
.thenApply(x -> true)
.exceptionally(t -> {
// If the inaccessible entry point is into our space, remove the entry point,
// and the dir/file it points to
// first make it writable by combining with the root writing key
getByPath("/" + username).thenCompose(rootDir ->
new FileTreeNode(file.getPointer(), file.getOwner(), e.readers, e.writers, rootDir.get().getEntryWriterKey())
.remove(network, null)
.thenApply(x -> removeFromStaticData(file)));
return true;
});
});
}
public static CompletableFuture<CommittedWriterData> getWriterData(NetworkAccess network, PublicSigningKey signer) {
return getWriterDataCbor(network, signer)
.thenApply(pair -> new CommittedWriterData(MaybeMultihash.of(pair.left), WriterData.fromCbor(pair.right, null)));
}
private static CompletableFuture<Pair<Multihash, CborObject>> getWriterDataCbor(NetworkAccess network, String username) {
return network.coreNode.getPublicKey(username)
.thenCompose(signer -> {
PublicSigningKey publicSigningKey = signer.orElseThrow(
() -> new IllegalStateException("No public-key for user " + username));
return getWriterDataCbor(network, publicSigningKey);
});
}
private static CompletableFuture<Pair<Multihash, CborObject>> getWriterDataCbor(NetworkAccess network, PublicSigningKey signer) {
return network.mutable.getPointer(signer)
.thenCompose(key -> network.dhtClient.get(key.get())
.thenApply(Optional::get)
.thenApply(cbor -> new Pair<>(key.get(), cbor))
);
}
@JsMethod
public CompletableFuture<Boolean> unfollow(String friendName) {
System.out.println("Unfollowing: "+friendName);
// remove entry point from static data
String friendPath = "/" + friendName + "/";
return getByPath(friendPath)
// remove our static data entry storing that they've granted us access
.thenCompose(dir -> removeFromStaticData(dir.get()))
.thenApply(b -> {
entrie = entrie.removeEntry(friendPath);
return true;
});
}
@JsMethod
public CompletableFuture<CommittedWriterData> removeFollower(String username) {
System.out.println("Remove follower: " + username);
// remove /$us/shared/$them
return getSharingFolder()
.thenCompose(sharing -> getByPath("/"+this.username+"/shared/"+username)
.thenCompose(dir -> dir.get().remove(network, sharing)
// remove our static data entry storing that we've granted them access
.thenCompose(b -> removeFromStaticData(dir.get()))));
}
public void logout() {
entrie = entrie.clear();
}
@JsMethod
public Fragmenter fragmenter() {
return fragmenter;
}
}