package peergos.shared.corenode;
import peergos.shared.cbor.*;
import peergos.shared.crypto.*;
import peergos.shared.crypto.asymmetric.*;
import peergos.shared.util.*;
import java.io.*;
import java.time.*;
import java.util.*;
import java.util.stream.*;
public class UserPublicKeyLink implements Cborable{
public static final int MAX_SIZE = 2*1024*1024;
public static final int MAX_USERNAME_SIZE = 64;
public final PublicSigningKey owner;
public final UsernameClaim claim;
private final Optional<byte[]> keyChangeProof;
public UserPublicKeyLink(PublicSigningKey owner, UsernameClaim claim, Optional<byte[]> keyChangeProof) {
this.owner = owner;
this.claim = claim;
this.keyChangeProof = keyChangeProof;
// check validity of link
if (keyChangeProof.isPresent()) {
PublicSigningKey newKeys = PublicSigningKey.fromByteArray(owner.unsignMessage(keyChangeProof.get()));
}
}
public UserPublicKeyLink(PublicSigningKey owner, UsernameClaim claim) {
this(owner, claim, Optional.empty());
}
public Optional<byte[]> getKeyChangeProof() {
return keyChangeProof.map(x -> Arrays.copyOfRange(x, 0, x.length));
}
@Override
public CborObject toCbor() {
Map<String, CborObject> values = new TreeMap<>();
values.put("owner", owner.toCbor());
values.put("claim", claim.toCbor());
keyChangeProof.ifPresent(proof -> values.put("keychange", new CborObject.CborByteArray(proof)));
return CborObject.CborMap.build(values);
}
public static UserPublicKeyLink fromCbor(CborObject cbor) {
if (! (cbor instanceof CborObject.CborMap))
throw new IllegalStateException("Invalid cbor for UserPublicKeyLink: " + cbor);
SortedMap<CborObject, CborObject> values = ((CborObject.CborMap) cbor).values;
PublicSigningKey owner = PublicSigningKey.fromCbor(values.get(new CborObject.CborString("owner")));
UsernameClaim claim = UsernameClaim.fromCbor(values.get(new CborObject.CborString("claim")));
CborObject.CborString proofKey = new CborObject.CborString("keychange");
Optional<byte[]> keyChangeProof = values.containsKey(proofKey) ?
Optional.of(((CborObject.CborByteArray)values.get(proofKey)).value) : Optional.empty();
return new UserPublicKeyLink(owner, claim, keyChangeProof);
}
@Deprecated
public byte[] toByteArray() {
try {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);
Serialize.serialize(claim.toByteArray(), dout);
dout.writeBoolean(keyChangeProof.isPresent());
if (keyChangeProof.isPresent())
Serialize.serialize(keyChangeProof.get(), dout);
return bout.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserPublicKeyLink that = (UserPublicKeyLink) o;
return Arrays.equals(toByteArray(), that.toByteArray());
}
@Override
public int hashCode() {
return Arrays.hashCode(toByteArray());
}
@Deprecated
public static UserPublicKeyLink fromByteArray(PublicSigningKey owner, byte[] raw) {
try {
DataInputStream din = new DataInputStream(new ByteArrayInputStream(raw));
UsernameClaim proof = UsernameClaim.fromByteArray(owner, Serialize.deserializeByteArray(din, MAX_SIZE));
boolean hasLink = din.readBoolean();
Optional<byte[]> link = hasLink ? Optional.of(Serialize.deserializeByteArray(din, MAX_SIZE)) : Optional.empty();
return new UserPublicKeyLink(owner, proof, link);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static List<UserPublicKeyLink> createChain(SigningKeyPair oldUser, SigningKeyPair newUser, String username, LocalDate expiry) {
// sign new claim to username, with provided expiry
UsernameClaim newClaim = UsernameClaim.create(username, newUser, expiry);
// sign new key with old
byte[] link = oldUser.signMessage(newUser.publicSigningKey.serialize());
// create link from old that never expires
UserPublicKeyLink fromOld = new UserPublicKeyLink(oldUser.publicSigningKey, UsernameClaim.create(username, oldUser, LocalDate.MAX), Optional.of(link));
return Arrays.asList(fromOld, new UserPublicKeyLink(newUser.publicSigningKey, newClaim));
}
public static class UsernameClaim implements Cborable {
public final String username;
public final LocalDate expiry;
private final byte[] signedContents;
public UsernameClaim(String username, LocalDate expiry, byte[] signedContents) {
this.username = username;
this.expiry = expiry;
this.signedContents = signedContents;
}
@Override
public CborObject toCbor() {
return new CborObject.CborList(Arrays.asList(new CborObject.CborString(username),
new CborObject.CborString(expiry.toString()),
new CborObject.CborByteArray(signedContents)));
}
public static UsernameClaim fromCbor(CborObject cbor) {
if (! (cbor instanceof CborObject.CborList))
throw new IllegalStateException("Invalid cbor for Username claim: " + cbor);
String username = ((CborObject.CborString)((CborObject.CborList) cbor).value.get(0)).value;
LocalDate expiry = LocalDate.parse(((CborObject.CborString)((CborObject.CborList) cbor).value.get(1)).value);
byte[] signedContents = ((CborObject.CborByteArray)((CborObject.CborList) cbor).value.get(2)).value;
return new UsernameClaim(username, expiry, signedContents);
}
@Deprecated
public static UsernameClaim fromByteArray(PublicSigningKey from, byte[] raw) {
try {
DataInputStream rawdin = new DataInputStream(new ByteArrayInputStream(raw));
byte[] signed = Serialize.deserializeByteArray(rawdin, MAX_SIZE);
byte[] unsigned = from.unsignMessage(signed);
DataInputStream din = new DataInputStream(new ByteArrayInputStream(unsigned));
String username = Serialize.deserializeString(din, CoreNode.MAX_USERNAME_SIZE);
LocalDate expiry = LocalDate.parse(Serialize.deserializeString(din, 100));
return new UsernameClaim(username, expiry, signed);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Deprecated
public byte[] toByteArray() {
try {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);
Serialize.serialize(signedContents, dout);
return bout.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static UsernameClaim create(String username, SigningKeyPair from, LocalDate expiryDate) {
try {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);
Serialize.serialize(username, dout);
Serialize.serialize(expiryDate.toString(), dout);
byte[] payload = bout.toByteArray();
byte[] signed = from.signMessage(payload);
return new UsernameClaim(username, expiryDate, signed);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UsernameClaim that = (UsernameClaim) o;
return Arrays.equals(toByteArray(), that.toByteArray());
}
@Override
public int hashCode() {
return Arrays.hashCode(toByteArray());
}
}
public static List<UserPublicKeyLink> createInitial(SigningKeyPair signer, String username, LocalDate expiry) {
UsernameClaim newClaim = UsernameClaim.create(username, signer, expiry);
return Collections.singletonList(new UserPublicKeyLink(signer.publicSigningKey, newClaim));
}
public static List<UserPublicKeyLink> merge(List<UserPublicKeyLink> existing, List<UserPublicKeyLink> tail) {
if (existing.size() == 0)
return tail;
if (!tail.get(0).owner.equals(existing.get(existing.size()-1).owner))
throw new IllegalStateException("Different keys in merge chains intersection!");
List<UserPublicKeyLink> result = Stream.concat(existing.subList(0, existing.size() - 1).stream(), tail.stream()).collect(Collectors.toList());
validChain(result, tail.get(0).claim.username);
return result;
}
public static void validChain(List<UserPublicKeyLink> chain, String username) {
for (int i=0; i < chain.size()-1; i++)
if (!validLink(chain.get(i), chain.get(i+1).owner, username))
throw new IllegalStateException("Invalid public key chain link!");
if (!validClaim(chain.get(chain.size()-1), username))
throw new IllegalStateException("Invalid username claim!");
}
static boolean validLink(UserPublicKeyLink from, PublicSigningKey target, String username) {
if (!validClaim(from, username))
return true;
Optional<byte[]> keyChangeProof = from.getKeyChangeProof();
if (!keyChangeProof.isPresent())
return false;
PublicSigningKey targetKey = PublicSigningKey.fromByteArray(from.owner.unsignMessage(keyChangeProof.get()));
if (!Arrays.equals(targetKey.serialize(), target.serialize()))
return false;
return true;
}
static boolean validClaim(UserPublicKeyLink from, String username) {
if (username.contains(" ") || username.contains("\t") || username.contains("\n"))
return false;
if (username.length() > MAX_USERNAME_SIZE)
return false;
if (!from.claim.username.equals(username) || from.claim.expiry.isBefore(LocalDate.now()))
return false;
return true;
}
}