Selaa lähdekoodia

initial commit

Billy Barrow 2 vuotta sitten
sitoutus
7abf695459

+ 8 - 0
.gitignore

@@ -0,0 +1,8 @@
+meson-*
+/lib
+/tools
+*.ninja
+.ninja_*
+compile_commands.json
+/extract
+/build

+ 3 - 0
README.md

@@ -0,0 +1,3 @@
+# Riddle
+
+A system for finding application peers and resolving names over mesh-like networks

+ 18 - 0
src/infra/gen-domain/domain-gen.vala

@@ -0,0 +1,18 @@
+using Riddle;
+
+public static int main(string[] args) {
+
+    var name = args[1];
+    var ip = args[2];
+    var key = args.length > 3 ? args[3] : DecentralisedNameInfo.generate_key();
+    var expire = new DateTime.now_utc().add_days(90);
+
+    var props = new Invercargill.Sequence<NameProperty>();
+    props.add(new NameProperty(NamePropertyType.ADDRESS, ip, new string[0]));
+
+    var info = new DecentralisedNameInfo(name, key, expire, props);
+
+    print(@"Domain generation summary:\n\tPrivate Key: \t$key\n\tDomain Name: \t$(info.name)\n\tName Info:\t$(info.get_encoded())\n");
+
+    return 0;
+}

+ 3 - 0
src/infra/gen-domain/meson.build

@@ -0,0 +1,3 @@
+sources = files('domain-gen.vala')
+
+executable('create-decentralised-domain', sources, dependencies: dependencies)

+ 2 - 0
src/infra/meson.build

@@ -0,0 +1,2 @@
+subdir('server')
+subdir('gen-domain')

+ 18 - 0
src/infra/resolver/resolver.py

@@ -0,0 +1,18 @@
+import dnslib.server
+import dnslib.dns
+import dnslib
+
+class TestResolver(dnslib.server.BaseResolver):
+     def resolve(self,request: dnslib.dns.DNSRecord,handler: dnslib.server.DNSHandler):
+        print(type(request), type(handler))
+        print(type(request.questions[0]))
+        print(request.questions[0])
+        reply = request.reply()
+        reply.add_answer(*dnslib.RR.fromZone("abc.com 60 A 1.2.3.4"))
+        reply.header.rcode = getattr(dnslib.RCODE,'NXDOMAIN')
+        return reply
+
+
+resolver = TestResolver()
+server = dnslib.server.DNSServer(resolver,port=8053,address="::1",tcp=False)
+server.start()

+ 3 - 0
src/infra/server/meson.build

@@ -0,0 +1,3 @@
+sources = files('server.vala')
+
+executable('standalone_server', sources, dependencies: dependencies)

+ 20 - 0
src/infra/server/server.vala

@@ -0,0 +1,20 @@
+using Riddle;
+
+public static int main(string[] args) {
+
+    if(args.length != 2) {
+        printerr("Please specify port only\n");
+        return -1;
+    }
+
+    var server = new Server((uint16)int.parse(args[1]));
+
+    var loop = new MainLoop();
+    server.run.begin((obj, res) => {
+            server.run.end(res);
+            loop.quit();
+        });
+    loop.run();
+
+    return 0;
+}

+ 23 - 0
src/lib/Answer.vala

@@ -0,0 +1,23 @@
+namespace Riddle {
+
+
+    public class Answer {
+
+        public string identifier { get; set; }
+        public uint8[] data { get; set; }
+
+        public Answer.from_arguments(string[] arguments) {
+            identifier = arguments[0];
+            data = Base64.decode (arguments[1]);
+        }
+
+        public string[] to_arguments() {
+            return new string[] {
+                identifier,
+                Base64.encode(data)
+            };
+        }
+
+    }
+
+}

+ 18 - 0
src/lib/CertifiedNameInfo.vala

@@ -0,0 +1,18 @@
+using Invercargill;
+
+namespace Riddle {
+
+    public class CertifiedNameInfo : NameInfo {
+
+        private uint8[] raw_data { get; protected set; }
+
+        public override string get_encoded() {            
+            return Base64.encode(raw_data);
+        }
+
+        // TODO support for verifying using signed name info and verifying against system-trusted x.509 certificates
+
+
+    }
+
+}

+ 22 - 0
src/lib/Challenge.vala

@@ -0,0 +1,22 @@
+namespace Riddle {
+
+
+    public class Challenge {
+
+        public string identifier { get; set; }
+        public uint8[] data { get; set; }
+
+        public Challenge.from_string(string challenge) {
+            var parts = challenge.split(" ", 2);
+            identifier = parts[0];
+            data = Base64.decode (parts[1]);
+        }
+
+        public string to_string() {
+            var encoded_data = Base64.encode(data);
+            return @"$identifier $encoded_data";
+        }
+
+    }
+
+}

+ 9 - 0
src/lib/Client.vala

@@ -0,0 +1,9 @@
+using Invercargill;
+
+namespace Riddle {
+
+    public class Client {
+
+    }
+
+}

+ 111 - 0
src/lib/DecentralisedNameInfo.vala

@@ -0,0 +1,111 @@
+using Invercargill;
+
+namespace Riddle {
+
+    public class DecentralisedNameInfo : NameInfo {
+
+        public uint8[] public_key { get; protected set; }
+        private uint8[] raw_data { get; protected set; }
+
+        public override string get_encoded() {
+            return Base64.encode(raw_data);
+        }
+
+
+        public DecentralisedNameInfo.from_string(string encoded_name_info) throws NameInfoError {
+            raw_data = Base64.decode (encoded_name_info);
+            public_key = raw_data[0:Sodium.Asymmetric.Signing.PUBLIC_KEY_BYTES];
+            var signed_message = raw_data[Sodium.Asymmetric.Signing.PUBLIC_KEY_BYTES:];
+            var data = Sodium.Asymmetric.Signing.verify (signed_message, public_key);
+
+            if(data == null) {
+                throw new NameInfoError.BAD_DATA("Invalid signature");
+            }
+
+            parse_base_info ((string)data);
+            validate_base_info();
+
+            if(!name.has_suffix (".rns")) {
+                throw new NameInfoError.INVALID("Decentralised names must end in '.rns'.");
+            }
+
+            var expected_suffix = generate_suffix(get_root_name(name), public_key);
+            if(!name.has_suffix (expected_suffix)) {
+                throw new NameInfoError.INVALID("The name and private key combination is not valid.");
+            }
+
+        }
+
+        public DecentralisedNameInfo(string name, string key, DateTime expiry, Enumerable<NameProperty> properties) {
+            var keys = key.split(":");
+            public_key = Base64.decode(keys[0]);
+            var secret_key = Base64.decode(keys[1]);
+            var name_parts = name.split(".");
+            var root_name = name_parts[name_parts.length-1];
+
+            this.name = name + generate_suffix(root_name, public_key);
+            expires = expiry.to_utc();
+            effective = new DateTime.now_utc();
+            this.properties = properties.to_sequence();
+
+            var data = stringify_base_info();
+            var signed = Sodium.Asymmetric.Signing.sign (data.data, secret_key);
+
+            // Data is public-key followed by signed data
+            raw_data = new uint8[public_key.length + signed.length];
+            for(var i = 0; i < raw_data.length; i++) {
+                if(i < public_key.length) {
+                    raw_data[i] = public_key[i];
+                }
+                else {
+                    raw_data[i] = signed[i-public_key.length];
+                }
+            }
+        }
+
+
+        public static string generate_key() {
+            var pk = new uint8[Sodium.Asymmetric.Signing.PUBLIC_KEY_BYTES];
+            var sk = new uint8[Sodium.Asymmetric.Signing.SECRET_KEY_BYTES];
+            Sodium.Asymmetric.Signing.generate_keypair (pk, sk);
+            
+            var pk_enc = Base64.encode (pk);
+            var sk_enc = Base64.encode (sk);
+
+            return @"$pk_enc:$sk_enc";
+        }
+
+        public static string generate_suffix(string root_name, uint8[] public_key) {
+            var checksum = new Checksum(ChecksumType.SHA512);
+            var name_data = root_name.data;
+
+            checksum.update(name_data, name_data.length);
+            checksum.update(public_key, public_key.length);
+            var digest = new uint8[64];
+            size_t size = digest.length;
+            checksum.get_digest(digest, ref size);
+
+            var suffix = @".$(encode_hash(digest)).rns";
+            return suffix;
+        }
+
+        public static string get_root_name(string name) {
+            var parts = name.split(".");
+            return parts[parts.length - 3];
+        }
+
+        private const string ENCODING_CHARS = "abcdefghijklmnopqrstuvwxyz234567";
+        private static string encode_hash(uint8[] data) {
+            var s = new char[6];
+            for(var i = 0; i < 6; i++) {
+                var v = data[i*10];
+                var ev = v / 8;
+                s[i] = ENCODING_CHARS[ev];
+            }
+
+            return (string)s;
+        }
+
+    }
+
+}

+ 82 - 0
src/lib/IdentityRiddle.vala

@@ -0,0 +1,82 @@
+namespace Riddle {
+
+    public class IdentityRiddle : Riddle {
+
+
+        private uint8[]? private_key { get; set; }
+        private uint8[] public_key { get; set; }
+
+        public IdentityRiddle.from_identity(string identiy) {
+            var parts = identiy.split(":", 2);
+            public_key = Base64.decode (parts[0]);
+            private_key = Base64.decode (parts[1]);
+        }
+
+        public IdentityRiddle(string public_key) {
+            this.public_key = Base64.decode (public_key);
+        }
+
+        public static string generate_identity() {
+            var pk = new uint8[Sodium.Asymmetric.Signing.PUBLIC_KEY_BYTES];
+            var sk = new uint8[Sodium.Asymmetric.Signing.SECRET_KEY_BYTES];
+            Sodium.Asymmetric.Signing.generate_keypair (pk, sk);
+            
+            var pk_enc = Base64.encode (pk);
+            var sk_enc = Base64.encode (sk);
+
+            return @"$pk_enc:$sk_enc";
+        }
+
+        public static string get_public_key_from_identity(string identity) {
+            var parts = identity.split(":", 2);
+            return parts[0];
+        }
+
+        public override Challenge create_challenge () {
+            var salt = new uint8[5120];
+            Sodium.Random.random_bytes(salt);
+
+            var challenge = new Challenge();
+            challenge.identifier = "riddle-identity-" + Base64.encode (public_key);
+            challenge.data = salt;
+            return challenge;
+        }
+        public override Answer? answer_challenge (Challenge challenge) {
+            if(private_key == null) {
+                warning ("Attempted to answer challenge for an identiy riddle with no private key");
+                return null;
+            }
+            if(!challenge.identifier.has_prefix ("riddle-identity-")) {
+                return null;
+            }
+            if(challenge.identifier[16:] != Base64.encode (public_key)) {
+                return null;
+            }
+
+            var answer = new Answer ();
+            answer.identifier = challenge.identifier;
+
+            var data = join_data(challenge.data, serialise_address());
+            answer.data = Sodium.Asymmetric.Signing.sign (data, private_key);
+
+            return answer;        
+        }
+        public override GLib.InetSocketAddress? verify_answer (Challenge challenge, Answer answer) {
+            
+            var verified = Sodium.Asymmetric.Signing.verify (answer.data, public_key);
+            if(verified == null) {
+                return null;
+            }
+
+            for(var i = 0; i < challenge.data.length; i++) {
+                if(challenge.data[i] != verified[i]) {
+                    return null;
+                }
+            }
+
+            return deserialise_address (verified[challenge.data.length:]);
+        }
+
+    }
+
+}

+ 140 - 0
src/lib/Message.vala

@@ -0,0 +1,140 @@
+using Invercargill;
+
+namespace Riddle {
+
+
+    public enum MessageType {
+        JOIN,
+        LEAVE,
+        PROPOGATE,
+        WHO_IN,
+        WHO_IS,
+        CHALLENGE,
+
+        OK,
+        ERROR,
+        SEE_ALSO,
+        ANSWER,
+        NOT_ACCEPTED,
+        UNKNOWN;
+
+        public static MessageType? from_string(string s) {
+            switch (s) {
+                case "JOIN":
+                    return MessageType.JOIN;
+                case "LEAVE":
+                    return MessageType.LEAVE;
+                case "PROPOGATE":
+                    return MessageType.PROPOGATE;
+                case "WHO-IN":
+                    return MessageType.WHO_IN;
+                case "WHO-IS":
+                    return MessageType.WHO_IS;
+                case "CHALLENGE":
+                    return MessageType.CHALLENGE;
+                case "OK":
+                    return MessageType.OK;
+                case "ERROR":
+                    return MessageType.ERROR;
+                case "SEE-ALSO":
+                    return MessageType.SEE_ALSO;
+                case "ANSWER":
+                    return MessageType.ANSWER;
+                case "NOT-ACCEPTED":
+                    return MessageType.NOT_ACCEPTED;
+                case "UNKNOWN":
+                    return MessageType.UNKNOWN;
+                default:
+                    return null;
+            }
+        }
+
+        public static string to_string(MessageType t) {
+            switch (t) {
+                case MessageType.JOIN:
+                    return "JOIN";
+                case MessageType.LEAVE:
+                    return "LEAVE";
+                case MessageType.PROPOGATE:
+                    return "PROPOGATE";
+                case MessageType.WHO_IN:
+                    return "WHO-IN";
+                case MessageType.WHO_IS:
+                    return "WHO-IS";
+                case MessageType.CHALLENGE:
+                    return "CHALLENGE";
+                case MessageType.OK:
+                    return  "OK";
+                case MessageType.ERROR:
+                    return "ERROR";
+                case MessageType.SEE_ALSO:
+                    return "SEE-ALSO";
+                case MessageType.ANSWER:
+                    return "ANSWER";
+                case MessageType.NOT_ACCEPTED:
+                    return "NOT-ACCEPTED";
+                case MessageType.UNKNOWN:
+                    return "UNKNOWN";
+                default:
+                    assert_not_reached();
+            }
+        }
+    }
+
+    public class Message {
+
+        public MessageType message_type { get; set; }
+        public string[] arguments { get; set; }
+        public string[] items { get; set; }
+
+        public Message.from_lines(string[] lines) throws Error {
+            var header = lines[0].chomp().chug().split(" ");
+
+            var type = MessageType.from_string(header[0]);
+            if(type == null) {
+                throw new Error(Quark.from_string("invalid-message"), 2, "Invalid message type");
+            }
+
+            message_type = type;
+            arguments = header[1:];
+            items = lines[1:];
+        }
+
+        public Message(MessageType message_type, string[] args, string[] data) {
+            this.message_type = message_type;
+            this.arguments = args;
+            this.items = data;
+        }
+
+        public string to_string() {
+
+            var type_string = MessageType.to_string(message_type);
+            var args = ate(arguments).to_string(s => s, " ");
+            var data = ate(items).to_string(s => s, "\n");
+            if(data == "") {
+                return @"$type_string $args";
+            }
+
+            return @"$type_string $args\n$data";
+        }
+
+        public static async Message from_stream(DataInputStream stream) throws Error {
+            var lines = new Invercargill.Sequence<string>();
+            var last_line = "";
+            while(true) {
+                last_line = yield stream.read_line_async();
+                if(last_line == "") {
+                    break;
+                }
+                lines.add(last_line);
+            }
+
+            return new Message.from_lines(lines.to_array());
+        }
+
+        public async void send(DataOutputStream stream) throws Error {
+            stream.put_string(@"$this\n\n");
+        }
+    }
+
+}

+ 128 - 0
src/lib/NameInfo.vala

@@ -0,0 +1,128 @@
+using Invercargill;
+
+namespace Riddle {
+
+    public errordomain NameInfoError {
+        BAD_DATA,
+        INVALID,
+        NOT_IN_DATE
+    }
+
+    public abstract class NameInfo {
+
+        public string name { get; protected set; }
+        public DateTime effective { get; protected set; }
+        public DateTime expires { get; protected set; }
+        public Enumerable<NameProperty> properties { get; protected set; }
+
+        public abstract string get_encoded();
+
+        protected string stringify_base_info() {
+            var info = @"$name\n$(effective.format_iso8601()) until $(expires.format_iso8601())";
+            foreach (var property in properties) {
+                info += @"\n$(property.to_string())";
+            }
+            return info;
+        }
+
+        protected void parse_base_info(string info) {
+            var data = info.split("\n");
+            name = data[0];
+            var range = data[1].split(" until ", 2);
+            effective = new DateTime.from_iso8601(range[0], null);
+            expires = new DateTime.from_iso8601(range[1], null);
+
+            var props = new Invercargill.Sequence<NameProperty>();
+            for(var i = 2; i < data.length; i++) {
+                props.add(new NameProperty.from_string(data[i]));
+            }
+
+            properties = props;
+        }
+
+        protected void validate_base_info() throws NameInfoError {
+            var current_time = new DateTime.now_utc();
+            if(current_time.difference(effective) < 0) {
+                throw new NameInfoError.NOT_IN_DATE("The name information is not effective yet.");
+            }
+            if(current_time.difference(expires) > 0) {
+                throw new NameInfoError.NOT_IN_DATE("The name information has expired.");
+            }
+        }
+
+    }
+
+    public class NameProperty {
+
+        public NamePropertyType property_type { get; set; }
+        public string value { get; set; }
+        public string[] arguments { get; set; }
+
+        public string to_string() {
+            var args = ate(arguments).to_string(s => s, " ");
+            return @"$(NamePropertyType.to_string(property_type)) $value $args";
+        }
+
+        public NameProperty.from_string(string property) {
+            var parts = property.split(" ");
+            property_type = NamePropertyType.from_string(parts[0]);
+            value = parts[1];
+            arguments = parts[2:];
+        }
+
+        public NameProperty(NamePropertyType prop_type, string value, string[] arguments) {
+            property_type = prop_type;
+            this.value = value;
+            this.arguments = arguments;
+        }
+
+    }
+
+    public enum NamePropertyType {
+        ADDRESS,
+        POINTER,
+        MAILSERVER,
+        TEXT,
+        LOCATION,
+        SERVICE;
+
+        public static NamePropertyType? from_string(string s) {
+            switch (s){
+                case "ADDRESS":
+                    return NamePropertyType.ADDRESS;
+                case "POINTER":
+                    return NamePropertyType.POINTER;
+                case "MAILSERVER":
+                    return NamePropertyType.MAILSERVER;
+                case "TEXT":
+                    return NamePropertyType.TEXT;
+                case "LOCATION":
+                    return NamePropertyType.LOCATION;
+                case "SERVICE":
+                    return NamePropertyType.SERVICE;
+                default:
+                    return null;
+            }
+        }
+
+        public static string to_string(NamePropertyType t) {
+            switch (t){
+                case NamePropertyType.ADDRESS:
+                    return "ADDRESS";
+                case NamePropertyType.POINTER:
+                    return "POINTER";
+                case NamePropertyType.MAILSERVER:
+                    return "MAILSERVER";
+                case NamePropertyType.TEXT:
+                    return "TEXT";
+                case NamePropertyType.LOCATION:
+                    return "LOCATION";
+                case NamePropertyType.SERVICE:
+                    return "SERVICE";
+                default:
+                    assert_not_reached();
+            }
+        }
+    }
+
+}

+ 37 - 0
src/lib/Riddle.vala

@@ -0,0 +1,37 @@
+namespace Riddle {
+
+    public abstract class Riddle {
+
+        public InetSocketAddress application_address { get; set; }
+
+        public abstract Challenge create_challenge();
+        public abstract Answer? answer_challenge(Challenge challenge);
+        public abstract InetSocketAddress? verify_answer(Challenge challenge, Answer answer);
+
+        protected uint8[] join_data(uint8[] payload, uint8[] to_add) {
+            var new_payload = new uint8[payload.length + to_add.length];
+            for(var i = 0; i < new_payload.length; i++) {
+                if(i < payload.length) {
+                    new_payload[i] = payload[i];
+                }
+                else {
+                    new_payload[i] = to_add[i - payload.length];
+                }
+            }
+
+            return new_payload;
+        }
+
+        protected uint8[] serialise_address() {
+            var s = @"$(application_address.address) $(application_address.port)";
+            return s.data;
+        }
+
+        protected InetSocketAddress deserialise_address(uint8[] data) {
+            var parts = ((string)data).split(" ", 2);
+            return new InetSocketAddress.from_string(parts[0], uint.parse(parts[1]));
+        }
+
+    }
+
+}

+ 234 - 0
src/lib/Server.vala

@@ -0,0 +1,234 @@
+using Invercargill;
+
+namespace Riddle {
+
+    public class Server {
+
+        public const int REGISTRATION_TIMEOUT_US = 600000000;
+        private SocketService service;
+        private Gee.HashMap<string, Invercargill.Sequence<Registration>> registrations = new Gee.HashMap<string, Invercargill.Sequence<Registration>>();
+        private Gee.HashMap<string, Invercargill.Sequence<Riddle>> riddles = new Gee.HashMap<string, Invercargill.Sequence<Riddle>>();
+        private Gee.HashMap<string, NameInfo> names = new Gee.HashMap<string, NameInfo>();
+        
+        
+        public Server(uint16 port) throws Error {
+            service = new SocketService ();
+            service.add_inet_port (port, null);
+            service.start ();
+        }
+
+        public async void run() {
+            while(true) {
+                SocketConnection connection = null;
+                try {
+                    print("Waiting for connection\n");
+                    connection = yield service.accept_async(null);
+                    print("New connection\n");
+                    var dis = new DataInputStream(connection.input_stream);
+                    var dos = new DataOutputStream(connection.output_stream);
+
+                    var message = yield Message.from_stream(dis);
+                    print(@"Received $(MessageType.to_string(message.message_type)) message\n");
+                    var reply = service_message(message, (InetSocketAddress)connection.get_remote_address());
+                    yield reply.send(dos);
+                    yield dos.flush_async();
+                    yield connection.close_async();
+                }
+                catch(Error e) {
+                    warning(@"Error servicing connection: $(e.message)");
+                    try{ 
+                        yield connection.close_async();
+                    }
+                    catch(Error e2) {
+                        warning(@"Error closing connection after initial error: $(e2.message)");
+                    }
+                }
+            }
+
+        }
+
+        private Message service_message(Message msg, InetSocketAddress origin) throws Error {
+
+            switch (msg.message_type) {
+                case MessageType.JOIN:
+                    return handle_register(msg, origin);
+                case MessageType.LEAVE:
+                    return handle_deregister(msg, origin);
+                case MessageType.PROPOGATE:
+                    return handle_propogate(msg);
+                case MessageType.WHO_IN:
+                    return handle_who_in(msg);
+                case MessageType.WHO_IS:
+                    return handle_who_is(msg);
+                case MessageType.CHALLENGE:
+                    return handle_challenge(msg);
+                default:
+                    return new Message(MessageType.ERROR, new string[] { "unknown-command" }, new string[0]);
+            }
+
+        }
+
+        private Message handle_register(Message msg, InetSocketAddress origin) throws Error {
+            cleanup_registrations();
+            var port = uint.parse(msg.arguments[1]);
+            add_registration(msg.arguments[0], new InetSocketAddress.from_string(origin.address.to_string(), port));
+
+            var relevent = get_group_registrations(msg.arguments[0])
+                .where(r => r.address.address.to_string() != origin.address.to_string() && r.address.port != port);
+
+            if(relevent.any()) {
+                return new Message(MessageType.SEE_ALSO, new string[0], relevent
+                    .select<string>(r => @"$(r.address.address.to_string()) $(r.address.port)")
+                    .to_array());
+            }
+
+            return new Message(MessageType.OK, new string[0], new string[0]);
+        }
+
+        private Message handle_deregister(Message msg, InetSocketAddress origin) throws Error {
+            remove_registration(msg.arguments[0], new InetSocketAddress.from_string(origin.address.to_string(), uint.parse(msg.arguments[1])));
+            return new Message(MessageType.OK, new string[0], new string[0]);
+        }
+
+        private Message handle_propogate(Message msg) throws Error {
+            var name = msg.arguments[0];
+            var ttl = int.parse(msg.arguments[1]);
+            var encoded_name_info = msg.items[0];
+
+            try {
+                if(name.has_suffix(".rns")) {
+                    var name_info = new DecentralisedNameInfo.from_string(encoded_name_info);
+                    if(name_info.name != name) {
+                        return new Message(MessageType.NOT_ACCEPTED, new string[] { "100", "name-mismatch" }, new string[0]);
+                    }
+                    if(!add_name_info_if_latest(name_info)) {
+                        return new Message(MessageType.NOT_ACCEPTED, new string[] { "104", "outdated" }, new string[0]);
+                    }
+                }
+                else {
+                    // TODO, implement Certified Names
+                    return new Message(MessageType.ERROR, new string[0], new string[0]);
+                }
+            }
+            catch(NameInfoError.BAD_DATA e) {
+                return new Message(MessageType.NOT_ACCEPTED, new string[] { "101", "malformed-information" }, new string[0]);
+            }
+            catch(NameInfoError.INVALID e) {
+                return new Message(MessageType.NOT_ACCEPTED, new string[] { "102", "invalid-information" }, new string[0]);
+            }
+            catch(NameInfoError.NOT_IN_DATE e) {
+                return new Message(MessageType.NOT_ACCEPTED, new string[] { "103", "outside-date-range" }, new string[0]);
+            }
+
+            return new Message(MessageType.OK, new string[0], new string[0]);
+        }
+
+        private Message handle_who_in(Message msg) throws Error {
+            cleanup_registrations();
+            var addresses = get_group_registrations(msg.arguments[0]).select<string>(r => @"$(r.address.address.to_string()) $(r.address.port)");
+            return new Message(MessageType.ANSWER, new string[0], addresses.to_array());
+        }
+
+        private Message handle_who_is(Message msg) throws Error {
+            var name = get_name_info(msg.arguments[0]);
+            if(name == null) {
+                return new Message(MessageType.UNKNOWN, new string[0], new string[0]);
+            }
+            return new Message(MessageType.ANSWER, new string[0], new string[] { name.get_encoded() });
+        }
+
+        private Message handle_challenge(Message msg) throws Error {
+            var group_id = msg.arguments[0];
+            var riddles_to_try = Invercargill.empty<Riddle>();
+            lock(riddles) {
+                if(!riddles.has_key(group_id)) {
+                    return new Message(MessageType.UNKNOWN, new string[0], new string[0]);
+                }
+                riddles_to_try = riddles[group_id].to_sequence();
+            }
+
+            var challenges = ate(msg.items).select<Challenge>(item => new Challenge.from_string(item));
+            var answers = challenges.select<Answer?>(c => riddles_to_try.select<Answer?>(r => r.answer_challenge(c)).first_or_default(a => a != null));
+
+            var answer = answers.first_or_default();
+            if(answer == null) {
+                return new Message(MessageType.UNKNOWN, new string[0], new string[0]);
+            }
+            return new Message(MessageType.ANSWER, answer.to_arguments(), new string[0]);
+        }
+
+        private void cleanup_registrations() {
+            lock(registrations) {
+                var copy = new Gee.HashMap<string, Invercargill.Sequence<Registration>>();
+                foreach (var group in registrations) {
+                    var time = new DateTime.now_utc();
+                    copy.set(group.key, group.value.where(r => time.difference(r.timestamp) < REGISTRATION_TIMEOUT_US).to_sequence());
+                }
+                registrations = copy;
+            }
+        }
+
+        private void add_registration(string group, InetSocketAddress address) {
+            var reg = new Registration() {
+                address = address,
+                timestamp = new DateTime.now_utc()
+            };
+
+            lock(registrations) {
+                if(!registrations.has_key(group)) {
+                    registrations.set(group, new Invercargill.Sequence<Registration>());
+                }
+
+                registrations[group].add(reg);
+            }
+        }
+
+        private void remove_registration(string group, InetSocketAddress address) {
+            lock(registrations) {
+                Invercargill.Sequence<Registration> group_regs;
+                registrations.unset(group, out group_regs);
+                registrations.set(group, group_regs.where(r => r.address.port != address.port || r.address.address.to_string() != address.address.to_string()).to_sequence());
+            }
+        }
+
+        private Enumerable<Registration> get_group_registrations(string group) {
+
+            lock(registrations) {
+                var regs = Invercargill.empty<Registration>();
+                if(registrations.has_key(group)) {
+                    regs = registrations[group].to_sequence();
+                }
+
+                return regs;
+            }
+        }
+
+        private bool add_name_info_if_latest(NameInfo info) {
+            lock(names) {
+                if(names.has_key(info.name) && names[info.name].effective.difference(info.effective) > 0) {
+                    return false;
+                }
+                names.set(info.name, info);
+            }
+            return true;
+        }
+
+        private NameInfo? get_name_info(string name) {
+            lock(names) {
+                if(names.has_key(name)) {
+                    return names[name];
+                }
+            }
+            return null;
+        }
+
+    }
+
+    private class Registration {
+
+        public InetSocketAddress address { get; set; }
+        public DateTime timestamp { get; set; }
+
+    }
+    
+}

+ 69 - 0
src/lib/SharedKeyPairRiddle.vala

@@ -0,0 +1,69 @@
+namespace Riddle {
+
+    public class SharedKeyPairRiddle : Riddle {
+
+
+        private uint8[] private_key { get; set; }
+        private uint8[] public_key { get; set; }
+
+        public SharedKeyPairRiddle(string keypair) {
+            var parts = keypair.split(":", 2);
+            public_key = Base64.decode (parts[0]);
+            private_key = Base64.decode (parts[1]);
+        }
+
+        public static string generate_keypair() {
+            var pk = new uint8[Sodium.Asymmetric.Signing.PUBLIC_KEY_BYTES];
+            var sk = new uint8[Sodium.Asymmetric.Signing.SECRET_KEY_BYTES];
+            Sodium.Asymmetric.Signing.generate_keypair (pk, sk);
+            
+            var pk_enc = Base64.encode (pk);
+            var sk_enc = Base64.encode (sk);
+
+            return @"$pk_enc:$sk_enc";
+        }
+
+        public override Challenge create_challenge () {
+            var salt = new uint8[5120];
+            Sodium.Random.random_bytes(salt);
+
+            var challenge = new Challenge();
+            challenge.identifier = "riddle-skp-" + Base64.encode (public_key);
+            challenge.data = salt;
+            return challenge;
+        }
+        public override Answer? answer_challenge (Challenge challenge) {
+            if(!challenge.identifier.has_prefix ("riddle-skp-")) {
+                return null;
+            }
+            if(challenge.identifier[11:] != Base64.encode (public_key)) {
+                return null;
+            }
+
+            var answer = new Answer ();
+            answer.identifier = challenge.identifier;
+
+            var data = join_data(challenge.data, serialise_address());
+            answer.data = Sodium.Asymmetric.Signing.sign (data, private_key);
+
+            return answer;        
+        }
+        public override GLib.InetSocketAddress? verify_answer (Challenge challenge, Answer answer) {
+            
+            var verified = Sodium.Asymmetric.Signing.verify (answer.data, public_key);
+            if(verified == null) {
+                return null;
+            }
+
+            for(var i = 0; i < challenge.data.length; i++) {
+                if(challenge.data[i] != verified[i]) {
+                    return null;
+                }
+            }
+
+            return deserialise_address (verified[challenge.data.length:]);
+        }
+
+    }
+
+}

+ 52 - 0
src/lib/meson.build

@@ -0,0 +1,52 @@
+vapi_dir = meson.current_source_dir() / 'vapi'
+
+add_project_arguments(['--enable-checking','--vapidir', vapi_dir], language: 'vala')
+
+dependencies = [
+    dependency('glib-2.0'),
+    dependency('gobject-2.0'),
+    dependency('gio-2.0'),
+    dependency('gee-0.8'),
+    dependency('invercargill'),
+    dependency('gnutls'),
+    meson.get_compiler('vala').find_library('libsodium', dirs: vapi_dir),
+    meson.get_compiler('c').find_library('sodium'),
+    meson.get_compiler('c').find_library('m')
+]
+
+sources = files('Riddle.vala')
+sources += files('SharedKeyPairRiddle.vala')
+sources += files('IdentityRiddle.vala')
+sources += files('Message.vala')
+sources += files('Answer.vala')
+sources += files('Challenge.vala')
+sources += files('Server.vala')
+sources += files('Client.vala')
+sources += files('NameInfo.vala')
+sources += files('CertifiedNameInfo.vala')
+sources += files('DecentralisedNameInfo.vala')
+
+
+
+riddle = shared_library('libriddle', sources,
+    name_prefix: '',
+    dependencies: dependencies,
+    install: true,
+    vala_gir: 'Riddle-1.0.gir',
+    install_dir: [true, true, true, true]
+)
+riddle_dep = declare_dependency(link_with: riddle, include_directories: include_directories('.'))
+
+pkg = import('pkgconfig')
+pkg.generate(riddle,
+    version : '0.1',
+    name : 'libriddle',)
+    
+g_ir_compiler = find_program('g-ir-compiler')
+custom_target('riddle typelib', command: [g_ir_compiler, '--shared-library=libriddle.so', '--output', '@OUTPUT@', meson.current_build_dir() / 'Riddle-1.0.gir'],
+              output: 'Riddle-1.0.typelib',
+              depends: riddle,
+              install: true,
+              install_dir: get_option('libdir') / 'girepository-1.0')
+
+dependencies += riddle_dep

+ 237 - 0
src/lib/vapi/libsodium.vapi

@@ -0,0 +1,237 @@
+/* Vala Bindings for LibSodium
+ * Copyright (c) 2020 Billy Barrow <billyb@pcthingz.com>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+
+ [CCode (cheader_filename = "sodium.h", lower_case_cprefix = "sodium_")]
+ namespace Sodium {
+ 
+   namespace Random {
+     [CCode (cname = "randombytes_SEEDBYTES")]
+     public const size_t SEED_BYTES;
+   
+     [CCode (cname = "randombytes_random")]
+     public uint32 random();
+   
+     [CCode (cname = "randombytes_uniform")]
+     public uint32 random_uniform(uint32 upper_bound);
+   
+     [CCode (cname = "randombytes_buf")]
+     public void random_bytes(uint8[] buffer);
+   
+     [CCode (cname = "randombytes_buf_deterministic")]
+     public void random_bytes_deterministic(uint8[] buffer, uint8[] seed);
+   }
+ 
+   namespace Symmetric {
+     [CCode (cname = "crypto_secretbox_KEYBYTES")]
+     public const size_t KEY_BYTES;
+ 
+     [CCode (cname = "crypto_secretbox_NONCEBYTES")]
+     public const size_t NONCE_BYTES;
+ 
+     [CCode (cname = "crypto_secretbox_MACBYTES")]
+     public const size_t MAC_BYTES;
+ 
+     [CCode (cname = "crypto_secretbox_keygen")]
+     private void key_gen([CCode (array_length = false)]uint8[] key);
+ 
+     public uint8[] generate_key() {
+       uint8[] key = new uint8[KEY_BYTES];
+       key_gen(key);
+       return key;
+     }
+ 
+     [CCode (cname = "crypto_secretbox_easy")]
+     private void secretbox(
+       [CCode (array_length = false)]uint8[] ciphertext,
+       uint8[] message,
+       [CCode (array_length = false)]uint8[] nonce,
+       [CCode (array_length = false)]uint8[] key
+     );
+ 
+     public uint8[] encrypt(uint8[] message, uint8[] key, uint8[] nonce)
+       requires (key.length == KEY_BYTES) 
+       requires (nonce.length == NONCE_BYTES)
+     {
+       // Initialise array for ciphertext
+       size_t ciphertext_size = MAC_BYTES + message.length;
+       uint8[] ciphertext = new uint8[ciphertext_size];
+ 
+       // Encrypt
+       secretbox(ciphertext, message, nonce, key);
+ 
+       // Return ciphertext
+       return ciphertext;
+     }
+ 
+     [CCode (cname = "crypto_secretbox_open_easy")]
+     private int secretbox_open(
+       [CCode (array_length = false)]uint8[] message,
+       uint8[] ciphertext,
+       [CCode (array_length = false)]uint8[] nonce,
+       [CCode (array_length = false)]uint8[] key
+     );
+ 
+     public uint8[]? decrypt(uint8[] ciphertext, uint8[] key, uint8[] nonce)
+       requires (ciphertext.length > MAC_BYTES)
+       requires (key.length == KEY_BYTES) 
+       requires (nonce.length == NONCE_BYTES)
+     {
+       // Initialise array for message
+       size_t message_size = ciphertext.length - MAC_BYTES;
+       uint8[] message = new uint8[message_size];
+ 
+       // Decrypt
+       int status = secretbox_open(message, ciphertext, nonce, key);
+ 
+       // Did it work?
+       if(status != 0) {
+         // No, return null
+         return null;
+       }
+ 
+       return message;
+     }
+   }
+   
+   namespace Asymmetric {
+ 
+     namespace Signing {
+ 
+         [CCode (cname = "crypto_sign_PUBLICKEYBYTES")]
+         public const size_t PUBLIC_KEY_BYTES;
+ 
+         [CCode (cname = "crypto_sign_SECRETKEYBYTES")]
+         public const size_t SECRET_KEY_BYTES;
+ 
+         [CCode (cname = "crypto_sign_BYTES")]
+         public const size_t MAX_HEADER_BYTES;
+ 
+         [CCode (cname = "crypto_sign_keypair")]
+         public void generate_keypair(
+             [CCode (array_length = false)]uint8[] public_key,
+             [CCode (array_length = false)]uint8[] secret_key)
+             requires (public_key.length == PUBLIC_KEY_BYTES)
+             requires (secret_key.length == SECRET_KEY_BYTES);
+             
+         [CCode (cname = "crypto_sign")]
+         private void sign_message(
+             [CCode (array_length = false)] uint8[] signed_message,
+             out int signature_length,
+             uint8[] message,
+             [CCode (array_length = false)] uint8[] secret_key
+         );
+ 
+         public uint8[] sign(
+             uint8[] message,
+             uint8[] secret_key)
+             requires (secret_key.length == SECRET_KEY_BYTES)
+         {
+             int signature_length;
+             uint8[] signed_message = new uint8[MAX_HEADER_BYTES + message.length];
+             sign_message(signed_message, out signature_length, message, secret_key);
+             signed_message.resize(signature_length);
+ 
+             return signed_message;
+         }
+ 
+         [CCode (cname = "crypto_sign_open")]
+         private int sign_open(
+             [CCode (array_length = false)] uint8[] message,
+             out int message_length,
+             uint8[] signed_message,
+             [CCode (array_length = false)] uint8[] public_key
+         );
+ 
+         public uint8[]? verify(
+             uint8[] signed_message,
+             uint8[] public_key)
+             requires (public_key.length == PUBLIC_KEY_BYTES)
+         {
+             int message_length;
+             uint8[] message = new uint8[signed_message.length];
+             if(sign_open(message, out message_length, signed_message, public_key) != 0) {
+                 return null;
+             }
+             message.resize(message_length);
+ 
+             return message;
+         }
+ 
+     }
+ 
+     namespace Sealing {
+ 
+         [CCode (cname = "crypto_box_PUBLICKEYBYTES")]
+         public const size_t PUBLIC_KEY_BYTES;
+ 
+         [CCode (cname = "crypto_box_SECRETKEYBYTES")]
+         public const size_t SECRET_KEY_BYTES;
+ 
+         [CCode (cname = "crypto_box_SEALBYTES")]
+         public const size_t HEADER_BYTES;
+ 
+         [CCode (cname = "crypto_box_keypair")]
+         public void generate_keypair(
+             [CCode (array_length = false)]uint8[] public_key,
+             [CCode (array_length = false)]uint8[] secret_key)
+             requires (public_key.length == PUBLIC_KEY_BYTES)
+             requires (secret_key.length == SECRET_KEY_BYTES);
+ 
+         [CCode (cname = "crypto_box_seal")]
+         private void seal_message(
+             [CCode (array_length = false)] uint8[] ciphertext,
+             uint8[] message,
+             [CCode (array_length = false)] uint8[] public_key
+         );
+ 
+         public uint8[] seal(uint8[] message, uint8[] public_key)
+             requires (public_key.length == PUBLIC_KEY_BYTES)
+         {
+             uint8[] ciphertext = new uint8[HEADER_BYTES + message.length];
+             seal_message(ciphertext, message, public_key);
+             return ciphertext;
+         }
+ 
+         [CCode (cname = "crypto_box_seal_open")]
+         private int seal_open(
+             [CCode (array_length = false)] uint8[] message,
+             uint8[] ciphertext,
+             [CCode (array_length = false)] uint8[] public_key,
+             [CCode (array_length = false)] uint8[] secret_key
+         );
+ 
+         public uint8[]? unseal(
+             uint8[] ciphertext,
+             uint8[] public_key,
+             uint8[] secret_key) 
+             requires (public_key.length == PUBLIC_KEY_BYTES)
+             requires (secret_key.length == SECRET_KEY_BYTES)
+             requires (ciphertext.length > HEADER_BYTES)
+         {
+             uint8[] message = new uint8[ciphertext.length - HEADER_BYTES];
+             if(seal_open(message, ciphertext, public_key, secret_key) != 0){
+                 return null;
+             }
+             return message;
+         }
+         
+     }
+ 
+   }
+   
+ 
+ }

+ 9 - 0
src/meson.build

@@ -0,0 +1,9 @@
+project('Riddle', 'vala', 'c')
+vapi_dir = meson.current_source_dir() / 'vapi'
+
+add_project_arguments(['--disable-warnings', '--enable-checking','--vapidir', vapi_dir], language: 'vala')
+
+
+subdir('lib')
+subdir('infra')
+subdir('tests')

+ 23 - 0
src/tests/DecentralisedNameInfoTests.vala

@@ -0,0 +1,23 @@
+using Riddle;
+
+void decentralised_name_info_tests() {
+
+    Test.add_func("/name_info/decentralised/aa", () => {
+        var key = DecentralisedNameInfo.generate_key();
+        var expire = new DateTime.now_utc().add_days(1);
+        var name_info = new DecentralisedNameInfo("riddle-test", key, expire, Invercargill.empty<NameProperty>());
+
+        print(@"Generated name: $(name_info.name)\n");
+        
+        var serialised = name_info.get_encoded();
+
+        try{
+            var name_info2 = new DecentralisedNameInfo.from_string(serialised);
+            assert_cmpstr(name_info.name, CompareOperator.EQ, name_info2.name);
+        }
+        catch(Error e) {
+            assert_no_error(e);
+        }
+
+    });
+}

+ 56 - 0
src/tests/IdentityRiddleTests.vala

@@ -0,0 +1,56 @@
+using Riddle;
+
+void identity_riddle_tests() {
+
+    Test.add_func("/riddle/identity_riddle/can_validate", () => {
+        var identity = IdentityRiddle.generate_identity();
+        var public_key = IdentityRiddle.get_public_key_from_identity(identity);
+
+        var riddle1 = new IdentityRiddle(public_key);
+        var riddle2 = new IdentityRiddle.from_identity(identity);
+        riddle2.application_address = new InetSocketAddress.from_string("127.0.0.1", 5050);
+
+        var challenge = riddle1.create_challenge();
+        var answer = riddle2.answer_challenge(challenge);
+
+        var address = riddle1.verify_answer(challenge, answer);
+        assert_nonnull(address);
+        assert_cmpstr("127.0.0.1", CompareOperator.EQ, address.address.to_string());
+        assert_cmpuint(5050, CompareOperator.EQ, address.port);
+    });
+
+    Test.add_func("/riddle/identity_riddle/will_not_answer", () => {
+        var identity1 = IdentityRiddle.generate_identity();
+        var identity2 = IdentityRiddle.generate_identity();
+        var public_key = IdentityRiddle.get_public_key_from_identity(identity1);
+
+        var riddle1 = new IdentityRiddle(public_key);
+        var riddle2 = new IdentityRiddle.from_identity(identity2);
+        riddle2.application_address = new InetSocketAddress.from_string("127.0.0.1", 5050);
+
+        var challenge = riddle1.create_challenge();
+        var answer = riddle2.answer_challenge(challenge);
+        assert_null(answer);
+    });
+
+    Test.add_func("/riddle/identity_riddle/will_not_validate", () => {
+        var identity1 = IdentityRiddle.generate_identity();
+        var identity2 = IdentityRiddle.generate_identity();
+
+        var riddle1 = new IdentityRiddle.from_identity(identity1);
+        var riddle2 = new IdentityRiddle.from_identity(identity2);
+        riddle1.application_address = new InetSocketAddress.from_string("127.0.0.1", 5050);
+        riddle2.application_address = new InetSocketAddress.from_string("127.0.0.1", 5050);
+
+        var challenge1 = riddle1.create_challenge();
+        var challenge2 = riddle2.create_challenge();
+
+        var answer1 = riddle1.answer_challenge(challenge1);
+        var answer2 = riddle2.answer_challenge(challenge2);
+
+        answer1.data = answer2.data;
+
+        var address = riddle1.verify_answer(challenge1, answer1);
+        assert_null(address);
+    });
+}

+ 54 - 0
src/tests/SharedKeyPairRiddleTests.vala

@@ -0,0 +1,54 @@
+using Riddle;
+
+void shared_key_pair_riddle_tests() {
+
+    Test.add_func("/riddle/shared_key_pair_riddle/can_validate", () => {
+        var keypair = SharedKeyPairRiddle.generate_keypair();
+
+        var riddle1 = new SharedKeyPairRiddle(keypair);
+        var riddle2 = new SharedKeyPairRiddle(keypair);
+        riddle2.application_address = new InetSocketAddress.from_string("127.0.0.1", 5050);
+
+        var challenge = riddle1.create_challenge();
+        var answer = riddle2.answer_challenge(challenge);
+
+        var address = riddle1.verify_answer(challenge, answer);
+        assert_nonnull(address);
+        assert_cmpstr("127.0.0.1", CompareOperator.EQ, address.address.to_string());
+        assert_cmpuint(5050, CompareOperator.EQ, address.port);
+    });
+
+    Test.add_func("/riddle/shared_key_pair_riddle/will_not_answer", () => {
+        var keypair1 = SharedKeyPairRiddle.generate_keypair();
+        var keypair2 = SharedKeyPairRiddle.generate_keypair();
+
+        var riddle1 = new SharedKeyPairRiddle(keypair1);
+        var riddle2 = new SharedKeyPairRiddle(keypair2);
+        riddle2.application_address = new InetSocketAddress.from_string("127.0.0.1", 5050);
+
+        var challenge = riddle1.create_challenge();
+        var answer = riddle2.answer_challenge(challenge);
+        assert_null(answer);
+    });
+
+    Test.add_func("/riddle/shared_key_pair_riddle/will_not_validate", () => {
+        var keypair1 = SharedKeyPairRiddle.generate_keypair();
+        var keypair2 = SharedKeyPairRiddle.generate_keypair();
+
+        var riddle1 = new SharedKeyPairRiddle(keypair1);
+        var riddle2 = new SharedKeyPairRiddle(keypair2);
+        riddle1.application_address = new InetSocketAddress.from_string("127.0.0.1", 5050);
+        riddle2.application_address = new InetSocketAddress.from_string("127.0.0.1", 5050);
+
+        var challenge1 = riddle1.create_challenge();
+        var challenge2 = riddle2.create_challenge();
+
+        var answer1 = riddle1.answer_challenge(challenge1);
+        var answer2 = riddle2.answer_challenge(challenge2);
+
+        answer1.data = answer2.data;
+
+        var address = riddle1.verify_answer(challenge1, answer1);
+        assert_null(address);
+    });
+}

+ 13 - 0
src/tests/TestRunner.vala

@@ -0,0 +1,13 @@
+
+public static int main(string[] args) {
+
+    Test.init(ref args);
+
+    shared_key_pair_riddle_tests();
+    identity_riddle_tests();
+    decentralised_name_info_tests();
+
+    Test.run();
+
+    return 0;
+}

+ 7 - 0
src/tests/meson.build

@@ -0,0 +1,7 @@
+
+sources = files('TestRunner.vala')
+sources += files('SharedKeyPairRiddleTests.vala')
+sources += files('IdentityRiddleTests.vala')
+sources += files('DecentralisedNameInfoTests.vala')
+
+executable('riddle-test-suite', sources, dependencies: dependencies, install: false)