package lbms.plugins.mldht.kad;
import static the8472.bencode.Utils.buf2ary;
import the8472.bencode.BDecoder;
import the8472.bencode.BEncoder;
import the8472.bencode.Utils;
import lbms.plugins.mldht.kad.messages.GetResponse;
import lbms.plugins.mldht.kad.messages.PutRequest;
import lbms.plugins.mldht.kad.utils.ThreadLocalUtils;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.Signature;
import java.security.SignatureException;
import java.util.Collections;
import java.util.Formatter;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import net.i2p.crypto.eddsa.EdDSAEngine;
import net.i2p.crypto.eddsa.EdDSAPrivateKey;
import net.i2p.crypto.eddsa.EdDSAPublicKey;
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec;
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
public class GenericStorage {
public static final long EXPIRATION_INTERVAL_SECONDS = 2*60*60;
public static StorageItem buildMutable(Object data, EdDSAPrivateKey key, byte[] salt, long sequenceNumber) throws InvalidKeyException, SignatureException {
ByteBuffer raw = new BEncoder().encode(data, 1000);
Signature sig = new EdDSAEngine();
sig.initSign(key);
Map<String, Object> p = new TreeMap<>();
if(salt != null)
p.put("salt", salt);
p.put("seq", sequenceNumber);
p.put("v", new BEncoder.RawData(raw));
ByteBuffer buf = new BEncoder().encode(p, 1500);
// trim d ... e
buf.position(buf.position() + 1);
buf.limit(buf.limit() - 1);
sig.update(buf.duplicate());
byte[] signature = sig.sign();
byte[] pubkey = new EdDSAPublicKey(new EdDSAPublicKeySpec(key.getA(), StorageItem.spec)).getA().toByteArray();
return new StorageItem(buf2ary(raw), pubkey, signature, salt, sequenceNumber);
}
/**
* @param data any bencodable object that encodes to 1000 <= bytes
*/
public static StorageItem buildImmutable(Object data) {
ByteBuffer raw = new BEncoder().encode(data, 1000);
return new StorageItem(buf2ary(raw));
}
public static class StorageItem {
StorageItem(byte[] value, byte[] pubkey, byte[] signature, byte[] salt, long sequenceNumber) {
Objects.requireNonNull(value);
Objects.requireNonNull(pubkey);
Objects.requireNonNull(signature);
this.value = value;
this.pubkey = pubkey;
this.signature = signature;
this.salt = salt;
this.sequenceNumber = sequenceNumber;
}
StorageItem(byte[] value) {
Objects.requireNonNull(value);
this.value = value;
salt = null;
pubkey = null;
}
public StorageItem(PutRequest req) {
expirationDate = System.currentTimeMillis() + EXPIRATION_INTERVAL_SECONDS*1000;
value = buf2ary(req.getValue());
if(req.getPubkey() != null) {
sequenceNumber = req.getSequenceNumber();
signature = req.getSignature();
salt = req.getSalt();
pubkey = req.getPubkey();
} else {
pubkey = null;
salt = null;
}
}
public StorageItem(GetResponse rsp, byte[] expectedSalt) {
value = buf2ary(rsp.getRawValue());
if(rsp.getPubkey() != null) {
sequenceNumber = rsp.getSequenceNumber();
signature = rsp.getSignature();
this.salt = expectedSalt;
pubkey = rsp.getPubkey();
} else {
pubkey = null;
this.salt = null;
}
}
long expirationDate;
long sequenceNumber = -1;
byte[] signature;
final byte[] pubkey;
final byte[] salt;
byte[] value;
public boolean mutable() {
return pubkey != null;
}
public static final EdDSAParameterSpec spec = EdDSANamedCurveTable.getByName("ed25519-sha-512");
public boolean validateSig() {
try {
Signature sig = new EdDSAEngine();
sig.initVerify(new EdDSAPublicKey(new EdDSAPublicKeySpec(pubkey, spec)));
// ("4:salt" length-of-salt ":" salt) "3:seqi" seq "e1:v" len ":" and the encoded value
Map<String, Object> p = new TreeMap<>();
if(salt != null)
p.put("salt", salt);
p.put("seq", sequenceNumber);
p.put("v", new BEncoder.RawData(ByteBuffer.wrap(value)));
ByteBuffer buf = new BEncoder().encode(p, 1500);
// trim d ... e
buf.position(buf.position() + 1);
buf.limit(buf.limit() - 1);
sig.update(buf);
return sig.verify(signature);
} catch (InvalidKeyException | SignatureException e) {
return false;
}
}
public long seq() {
return sequenceNumber;
}
public ByteBuffer getRawValue() {
return ByteBuffer.wrap(value).asReadOnlyBuffer();
}
public Object getDecodedValue() {
return new BDecoder().decodeAny(ByteBuffer.wrap(value));
}
public Key fingerprint() {
return GenericStorage.fingerprint(pubkey, salt, ByteBuffer.wrap(value));
}
public void debugString(Formatter f) {
f.format("%s mutable:%b seq:%d %n",
fingerprint(),
mutable(),
seq()
);
f.format("%s%n%n", Utils.stripToAscii(getRawValue()));
}
public long getSequenceNumber() {
return sequenceNumber;
}
public Optional<ByteBuffer> pubKey() {
return Optional.ofNullable(pubkey).map(ByteBuffer::wrap).map(ByteBuffer::asReadOnlyBuffer);
}
public Optional<ByteBuffer> salt() {
return Optional.ofNullable(salt).map(ByteBuffer::wrap).map(ByteBuffer::asReadOnlyBuffer);
}
public Optional<ByteBuffer> sig() {
return Optional.ofNullable(signature).map(ByteBuffer::wrap).map(ByteBuffer::asReadOnlyBuffer);
}
}
ConcurrentHashMap<Key, StorageItem> items = new ConcurrentHashMap<>();
enum UpdateResult {
SUCCESS,
IMMUTABLE_SUBSTITUTION_FAIL,
SIG_FAIL,
CAS_FAIL,
SEQ_FAIL;
}
public static Key fingerprint(byte[] pubkey, byte[] salt, ByteBuffer buf) {
MessageDigest dig = ThreadLocalUtils.getThreadLocalSHA1();
dig.reset();
if(pubkey != null) {
dig.update(pubkey);
if(salt != null)
dig.update(salt);
return new Key(dig.digest());
}
dig.update(buf.duplicate());
return new Key(dig.digest());
}
public UpdateResult putOrUpdate(Key k, StorageItem newItem, long expected) {
if(newItem.mutable() && !newItem.validateSig())
return UpdateResult.SIG_FAIL;
while(true) {
StorageItem oldItem = items.putIfAbsent(k, newItem);
if(oldItem == null)
return UpdateResult.SUCCESS;
if(oldItem.mutable()) {
if(!newItem.mutable())
return UpdateResult.IMMUTABLE_SUBSTITUTION_FAIL;
if(newItem.sequenceNumber < oldItem.sequenceNumber)
return UpdateResult.SEQ_FAIL;
if(expected >= 0 && oldItem.sequenceNumber >= 0 && oldItem.sequenceNumber != expected)
return UpdateResult.CAS_FAIL;
}
if(items.replace(k, oldItem, newItem))
break;
}
return UpdateResult.SUCCESS;
}
public Optional<StorageItem> get(Key k) {
return Optional.ofNullable(items.get(k));
}
public void cleanup() {
long now = System.currentTimeMillis();
items.entrySet().removeIf(entry -> {
return entry.getValue().expirationDate < now;
});
}
public Map<Key, StorageItem> getItems() {
return Collections.unmodifiableMap(items);
}
}