Преглед изворни кода

Start implementation of PPCL and PPRF

Billy Barrow пре 1 година
родитељ
комит
e518b27fe1

+ 292 - 0
src/lib/Ppcl.vala

@@ -0,0 +1,292 @@
+using Invercargill;
+using Invercargill.Convert;
+
+namespace Ppub {
+    
+    public errordomain CollectionError {
+        INVALID_COLLECTION_SIGNATURE,
+        INVALID_MEMBER,
+        INVALID_MEMBER_SIGNATURE,
+        INVALID_ID,
+        INVALID_FORMAT
+    }
+    
+    public class Collection {
+        public uint8[] id { get; private set; }
+        public int serial { get; private set; }
+        public Vector<string> domains { get; private set; }
+        public Vector<CollectionMember> members { get; private set; }
+        public uint8[] auth { get; private set; }
+        public Vector<CollectionPublication> publications { get; private set; }
+        public DateTime modified { get; private set; }
+
+        private void initialise() {
+            domains = new Vector<string>();
+            members = new Vector<CollectionMember>();
+            publications = new Vector<CollectionPublication>();
+        }
+
+        public void increment_serial() {
+            serial++;
+        }
+
+        public void touch() {
+            modified = new DateTime.now_local();
+        }
+
+        public Collection(out uint8[] private_signing_key) {
+            id = new uint8[Sodium.Asymmetric.Signing.PUBLIC_KEY_BYTES];
+            private_signing_key = new uint8[Sodium.Asymmetric.Signing.SECRET_KEY_BYTES];
+            Sodium.Asymmetric.Signing.generate_keypair(id, private_signing_key);
+
+            initialise();
+        }
+
+        public Collection.from_stream(DataInputStream stream) throws Error {
+            var line = stream.read_line();
+            var signed_portion = line;
+            var header = line.split(" ");
+
+            if(header.length < 3) {
+                throw new CollectionError.INVALID_FORMAT("PPCL header contains less than three items");
+            }
+            if(header[0] != "PPCL") {
+                throw new CollectionError.INVALID_FORMAT("PPCL magic number missing from start of stream");
+            }
+
+            id = Base64.decode(header[1]);
+            if(id.length != Sodium.Asymmetric.Signing.PUBLIC_KEY_BYTES) {
+                throw new CollectionError.INVALID_ID("Collection ID is invalid");
+            }
+
+            int serial_int = 0;
+            if(!int.try_parse(header[2], out serial_int)) {
+                throw new CollectionError.INVALID_FORMAT("Could not parse serial number");
+            }
+            serial = serial_int;
+
+            initialise();
+            
+            while(true) {
+                line = stream.read_line();
+                var entry = line.split(" ", 2);
+                if(entry.length != 2) {
+                    throw new CollectionError.INVALID_FORMAT(@"Malformed collection entry \"$(entry[0])\"");
+                }
+
+                if(entry[0] == "MEM") {
+                    members.add(new CollectionMember.from_string(entry[1]));
+                }
+                else if(entry[0] == "DOM") {
+                    domains.add(entry[1]);
+                }
+                else if(entry[0] == "SIG") {
+                    auth = Base64.decode(entry[1]);
+                    break;
+                }
+                else {
+                    warning(@"Unrecognised entry type $(entry[0]), ignoring");
+                }
+                
+                signed_portion += @"\n$line";
+            }
+
+            line = stream.read_line();
+            var modified_entry = line.split(" ", 2);
+            if(modified_entry.length != 2 || modified_entry[0] != "MOD") {
+                throw new CollectionError.INVALID_FORMAT(@"Malformed or missing last modified date");
+            }
+
+            modified = new DateTime.from_iso8601(modified_entry[1], null);
+
+            line = stream.read_line();
+            if(line != "" && line != null) {
+                throw new CollectionError.INVALID_FORMAT(@"Line following MOD entry not blank");
+            }
+
+            // Verify auth
+            var checksum = new BinaryData.from_byte_array(get_string_checksum(signed_portion));
+            var signed_checksum = Sodium.Asymmetric.Signing.verify(auth, id);
+            if(signed_checksum == null) {
+                throw new CollectionError.INVALID_COLLECTION_SIGNATURE("Could not verify signature portion of collection");
+            }
+            if(!checksum.equals(ate(signed_checksum))) {
+                throw new CollectionError.INVALID_COLLECTION_SIGNATURE("Signature checksum does not match calculated checksum");
+            }
+
+            if(line == null) {
+                return;
+            }
+
+            while(true) {
+                line = stream.read_line();
+                if(line == "" || line == null) {
+                    break;
+                }
+                var entry = line.split(" ", 2);
+
+                if(entry[0] == "PUB") {
+                    publications.add(new CollectionPublication.from_string(entry[1], members));
+                }
+                else {
+                    warning(@"Unrecognised entry type $(entry[0]), ignoring");
+                }
+            }
+        }
+
+        public string to_string(uint8[] signing_key) {
+            var strings = domains
+                .select<string>(d => @"DOM $d")
+                .concat(members.select<string>(m => m.to_string()));
+            var data = strings.to_string(s => s, "\n");
+            var pub_str = publications.to_string(p => p.to_string(), "\n");
+
+            var signed_portion = @"PPCL $(Base64.encode(id)) $serial";
+            if(data != "") {
+                signed_portion += @"\n$data";
+            }
+
+            auth = Sodium.Asymmetric.Signing.sign(get_string_checksum(signed_portion), signing_key);
+            
+            var full_str = @"$signed_portion\nSIG $(Base64.encode(auth))\nMOD $(modified.format_iso8601())\n";
+            if(pub_str != "") {
+                full_str += @"\n$pub_str\n";
+            }
+            return full_str;
+        }
+
+        internal static uint8[] get_string_checksum(string data) {
+            var signed_data = new BinaryData();
+            signed_data.append_string(data);
+            var checksum_calculator = new Checksum(ChecksumType.SHA512);
+            var arr = signed_data.to_array();
+            print(@"ARR.length $(arr.length), str $data\n");
+            checksum_calculator.update(arr, arr.length);
+            var checksum = new uint8[64];
+            size_t chk_len = checksum.length;
+            checksum_calculator.get_digest(checksum, ref chk_len);
+            checksum.length = (int)chk_len;
+            return checksum;
+        }
+
+    }
+
+    public class CollectionMember {
+        public string name { get; private set; }
+        public uint8[] public_key { get; private set; }
+
+        public string to_string() {
+            return @"MEM $name $(Base64.encode(public_key))";
+        }
+
+        public CollectionMember(string name, uint8[] public_key) {
+            this.name = name;
+            this.public_key = public_key;
+        }
+
+        public CollectionMember.from_string(string line) throws CollectionError {
+            var parts = line.split(" ");
+            if(parts.length < 2) {
+                throw new CollectionError.INVALID_FORMAT("Member line must contain at least two fields, name and public key");
+            }
+            name = parts[0];
+            public_key = Base64.decode(parts[1]);
+        }
+
+        public static void new_keypair(out uint8[] private_key, out uint8[] public_key) {
+            private_key = new uint8[Sodium.Asymmetric.Signing.SECRET_KEY_BYTES];
+            public_key = new uint8[Sodium.Asymmetric.Signing.PUBLIC_KEY_BYTES];
+            Sodium.Asymmetric.Signing.generate_keypair(public_key, private_key);
+        }
+    }
+
+    public class CollectionPublication {
+        public string file_name { get; private set; }
+        public DateTime publication_time { get; private set; }
+        public string member { get; private set; }
+        public uint8[] signature { get; private set; }
+        public uint8[] publication_checksum { get; private set; }
+
+        private string raw_line; 
+
+        public string to_string() {
+            return raw_line;
+        }
+
+        private string get_signed_portion() {
+            return @"$file_name: $member $(publication_time.format_iso8601())";
+        }
+
+        public static uint8[] generate_ppub_checksum(File ppub_file) throws Error {
+            var buffer = new uint8[10240];
+            var checksum = new Checksum(ChecksumType.SHA512);
+            var stream = ppub_file.read();
+            while(true) {
+                var read = stream.read(buffer);
+                if(read == 0) {
+                    break;
+                }
+                checksum.update(buffer, read);
+            }
+            size_t dig_len = 64;
+            var digest = new uint8[64];
+            checksum.get_digest(digest, ref dig_len);
+            digest.length = (int)dig_len;
+            return digest;
+        }
+
+        public CollectionPublication(string file_name, DateTime publication_time, string member, uint8[] ppub_checksum, uint8[] signing_key) {
+            this.file_name = file_name;
+            this.publication_time = publication_time;
+            this.member = member;
+            this.publication_checksum = ppub_checksum;
+            sign(signing_key);
+        }
+
+        private void sign(uint8[] signing_key) {
+            var signed_portion = get_signed_portion();
+            var line_checksum = Collection.get_string_checksum(signed_portion);
+            var to_sign = new BinaryData.from_byte_array(line_checksum);
+            to_sign.append_byte_array(publication_checksum);
+
+            signature = Sodium.Asymmetric.Signing.sign(to_sign.to_array(), signing_key);
+
+            raw_line = @"PUB $signed_portion $(Base64.encode(signature))";
+        }
+
+        public CollectionPublication.from_string(string data, Enumerable<CollectionMember> members) throws CollectionError{
+            raw_line = "PUB " + data;
+            var entry = data.split(": ", 2);
+            if(entry.length < 2) {
+                throw new CollectionError.INVALID_FORMAT("Malformed publication entry");
+            }
+            var parts = entry[1].split(" ");
+            if(parts.length < 3) {
+                throw new CollectionError.INVALID_FORMAT("Publication line must contain at least 3 properties, member, time, and signature");
+            }
+
+            file_name = entry[0];
+            member = parts[0];
+            publication_time = new DateTime.from_iso8601(parts[1], null);
+            signature = Base64.decode(parts[2]);
+
+            var collection_member = members.first_or_default(m => m.name == member);
+            if(collection_member == null) {
+                throw new CollectionError.INVALID_MEMBER(@"Undeclared member \"$member\" on publication entry");
+            }
+
+            var checksum = new BinaryData.from_byte_array(Collection.get_string_checksum(get_signed_portion()));
+            print(@"\t\t$(get_signed_portion()) == $(Base64.encode(checksum.to_array()))\n");
+            var signature_data = Sodium.Asymmetric.Signing.verify(signature, collection_member.public_key);
+            if(signature_data == null) {
+                throw new CollectionError.INVALID_COLLECTION_SIGNATURE("Invalid publication signature");
+            }
+            if(!checksum.equals(ate(signature_data[0:64]))) {
+                throw new CollectionError.INVALID_COLLECTION_SIGNATURE("Publication signature does not match metadata");
+            }
+
+            publication_checksum = signature_data[64:128].copy();
+        }
+    }
+
+}

+ 0 - 0
src/lib/Pprf/BeginUpload.vala


+ 26 - 0
src/lib/Pprf/CollectionMessage.vala

@@ -0,0 +1,26 @@
+using Invercargill;
+
+namespace Ppub.Pprf {
+
+    public abstract class CollectionMessage : Message {
+
+        public BinaryData collection_id { get; set; }
+
+        public override void deserialise (GLib.DataInputStream stream) throws Error {
+            base.deserialise (stream);
+            var id = new uint8[Sodium.Asymmetric.Signing.PUBLIC_KEY_BYTES];
+            stream.read(id);
+            collection_id = new BinaryData.from_byte_array (id);
+        }
+
+        public override uint64 calculate_size() {
+            return base.calculate_size() + Sodium.Asymmetric.Signing.PUBLIC_KEY_BYTES;
+        }
+        
+        public override void serialise(DataOutputStream stream) throws Error {
+            base.serialise(stream);
+            stream.write(collection_id.to_array());
+        }
+
+    }
+}

+ 0 - 0
src/lib/Pprf/EncryptedMessage.vala


+ 0 - 0
src/lib/Pprf/Failure.vala


+ 0 - 0
src/lib/Pprf/GetAsset.vala


+ 86 - 0
src/lib/Pprf/GetListing.vala

@@ -0,0 +1,86 @@
+using Invercargill;
+
+namespace Ppub.Pprf {
+
+    public enum GetListingColumn {
+        TITLE = (uint16)(1 << 0),
+        AUTHOR = (uint16)(1 << 1),
+        DESCRIPTION = (uint16)(1 << 2),
+        TIMESTAMP = (uint16)(1 << 3),
+        TAGS = (uint16)(1 << 4),
+        POSTER = (uint16)(1 << 5),
+        COPYRIGHT = (uint16)(1 << 6),
+        CHECKSUM = (uint16)(1 << 7),
+    }
+
+    public class GetListing : CollectionMessage {
+    
+        public uint16 columns { get; set; }
+        public uint32 skip { get; set; }
+        public uint8 take { get; set; }
+        public string? tag { get; set; }
+        public string? query { get; set; }
+    
+        public GetListing() {
+            message_type = MessageType.GET_LISTING;
+        }
+    
+        public override void deserialise (GLib.DataInputStream stream) throws Error {
+            base.deserialise (stream);
+            columns = stream.read_uint16();
+            skip = stream.read_uint32();
+            take = stream.read_byte();
+
+            var tag_len = stream.read_uint16();
+            if(tag_len > 0) {
+                var tag_data = new uint8[tag_len];
+                stream.read(tag_data);
+                tag = new BinaryData.from_byte_array(tag_data).to_raw_string();
+            }
+
+            var query_len = stream.read_uint16();
+            if(query_len > 0) {
+                var query_data = new uint8[query_len];
+                stream.read(query_data);
+                query = new BinaryData.from_byte_array(query_data).to_raw_string();
+            }
+        }
+    
+        public override uint64 calculate_size() {
+            var text_size = 0;
+            if(query != null) {
+                text_size += query.data.length;
+            }
+            if(tag != null) {
+                text_size += tag.data.length;
+            }
+            return base.calculate_size() + 
+                2 + // Columns
+                4 + // Skip
+                1 + // Take
+                2 + // Tag size
+                2 + // Query size
+                text_size; // Query + tag
+        }
+        
+        public override void serialise(DataOutputStream stream) throws Error {
+            base.serialise(stream);
+            stream.put_uint16(columns);
+            stream.put_uint32(skip);
+            stream.put_byte(take);
+
+            var tag_size = tag == null ? 0 : tag.data.length-1;
+            stream.put_uint16(tag_size);
+            if(tag_size > 0) {
+                stream.put_string(tag);
+            }
+
+            var query_size = query == null ? 0 : query.data.length-1;
+            stream.put_uint16(query_size);
+            if(query_size > 0) {
+                stream.put_string(query);
+            }
+
+        }
+    }
+}

+ 0 - 0
src/lib/Pprf/GetPublication.vala


+ 0 - 0
src/lib/Pprf/GetPublicationInformation.vala


+ 35 - 0
src/lib/Pprf/IdentifyServer.vala

@@ -0,0 +1,35 @@
+using Invercargill;
+
+namespace Ppub.Pprf {
+
+    public class IdentifyServer : Message {
+        public IdentifyServer() {
+            message_type = MessageType.IDENTIFY_SERVER;
+        }
+    }
+
+    public class ServerIdentity : Message {
+
+        public BinaryData public_key { get; private set; }
+
+        public ServerIdentity() {
+            message_type = MessageType.SERVER_IDENTITY;
+        }
+
+        public override void deserialise (GLib.DataInputStream stream) throws Error {
+            base.deserialise (stream);
+            var key = new uint8[Sodium.Asymmetric.Sealing.PUBLIC_KEY_BYTES];
+            stream.read(key);
+            public_key = new BinaryData.from_byte_array (key);
+        }
+
+        public override uint64 calculate_size() {
+            return base.calculate_size() + Sodium.Asymmetric.Sealing.PUBLIC_KEY_BYTES;
+        }
+        
+        public override void serialise(DataOutputStream stream) throws Error {
+            base.serialise(stream);
+            stream.write(public_key.to_array());
+        }
+    }
+}

+ 137 - 0
src/lib/Pprf/Message.vala

@@ -0,0 +1,137 @@
+using Invercargill;
+
+namespace Ppub.Pprf {
+
+    public errordomain PprfMessageError {
+        INVALID_HEADER
+    }
+
+    public class Message {
+        public uint64 size { get; private set; }
+        public MessageType message_type { get; protected set; }
+
+        public virtual uint64 calculate_size() {
+            return 1;
+        }
+
+        public virtual void deserialise(DataInputStream stream) throws Error {
+            var magic = new uint8[5];
+            stream.read(magic);
+            if(magic[0] != 'P' || magic[1] != 'P' || magic[2] != 'R' || magic[3] != 'F' || magic[4] != 0x00) {
+                throw new PprfMessageError.INVALID_HEADER("Stream does not start with PPRF magic number");
+            }
+
+            size = stream.read_uint64();
+            message_type = (MessageType)stream.read_byte();
+        }
+
+        public virtual void serialise(DataOutputStream stream) throws Error {
+            stream.write(new uint8[] { 'P', 'P', 'R', 'F', 0x00 });
+            size = calculate_size();
+            stream.put_uint64(size);
+            stream.put_byte(message_type);
+        }
+    }
+
+    public abstract class MessageBody : InputStream {
+        public uint64 body_size { get; set; }
+        public abstract BinaryData? get_next_chunk(GLib.Cancellable? cancellable) throws Error;
+        protected virtual void cleanup() {}
+
+        private BinaryData buffered = null;
+
+        public virtual DataInputStream as_stream() {
+            return new DataInputStream(this);
+        }
+
+        public override bool close(GLib.Cancellable? cancellable) {
+            cleanup();
+            return true;            
+        }
+
+        public override ssize_t read(uint8[] buffer, GLib.Cancellable? cancellable) throws IOError {
+            if(buffered != null && buffered.count() != 0) {
+                lock(buffered) {
+                    var size = buffered.write_to(buffer, buffer.length);
+                    buffered = buffered.slice((int)size, buffered.count());
+                    return (ssize_t)size;
+                }
+            }
+
+            try {
+                var data = get_next_chunk(cancellable);
+                if(data == null) {
+                    return 0;
+                }
+
+                var size = data.write_to(buffer, buffer.length);
+                if(data.count() != size) {
+                    buffered = data.slice((int)size, data.count());
+                }
+                return (ssize_t)size;
+            }
+            catch(PprfMessageError e) {
+                throw new IOError.INVALID_DATA(@"PPRF Message Error: $(e.message) ($(e.domain))");
+            }
+            catch(IOError e) {
+                throw e;
+            }
+            catch(Error e) {
+                throw new IOError.INVALID_DATA(@"Unhandeled error: $(e.message) ($(e.domain))");
+            }
+        }
+    }
+
+    public enum MessageType {
+
+        // 0 - 31: Public user requests
+        IDENTIFY_SERVER = 0,
+        GET_LISTING = 1,
+        GET_PUBLICATION_INFORMATION = 2,
+        GET_ASSET = 3,
+        GET_PUBLICATION = 4,
+
+        // 32 - 63: Administration requests
+        REGISTER_NAME = 32,
+        BEGIN_UPLOAD = 33,
+        FINALISE_UPLOAD = 34,
+        UPLOAD = 35,
+        PUBLISH = 36,
+        UNPUBLISH = 37,
+        UPDATE_COLLECTION = 38,
+
+        // 64 - 95: Encapsulated messages
+        ENCRYPTED_MESSAGE = 64,
+        SIGNED_MESSAGE = 65,
+
+        // 96 - 127: Server initiated requests
+        PROPOGATE_PUBLICATION = 96,
+        PROPOGATE_COLLECTION = 97,
+        SYNCHRONISE_COLLECTION = 98,
+        GET_PUBLICATIONS = 99,
+
+
+        // 128 - 159: Server responses to public user requests
+        SERVER_IDENTITY = 129,
+        COLLECTION_LISTING = 130,
+        PUBLICATION_INFORMATION = 131,
+        ASSET = 132,
+        PUBLICATION = 133,
+        TRY_FIRST = 134,
+
+        // 160 - 191: Server responses to administration requests
+        NAME_REGISTRATION = 160,
+        UPLOAD_SESSION = 161,
+
+        // 192 - 223: Generic server responses
+        CONFIRMATION = 192,
+        FAILURE = 193,
+
+        // 224 - 255: Server to server responses
+        COLLECTION_SYNCHRONISATION = 224,
+        BULK_PUBLICATIONS = 225
+
+
+    }
+
+}

+ 0 - 0
src/lib/Pprf/MessageFactory.vala


+ 0 - 0
src/lib/Pprf/Publish.vala


+ 0 - 0
src/lib/Pprf/RegisterName.vala


+ 0 - 0
src/lib/Pprf/SignedMessage.vala


+ 0 - 0
src/lib/Pprf/UpdateCollection.vala


+ 0 - 0
src/lib/Pprf/Upload.vala


+ 20 - 0
src/lib/meson.build

@@ -4,6 +4,8 @@ dependencies = [
     dependency('gio-2.0'),
     dependency('gee-0.8'),
     dependency('invercargill'),
+    meson.get_compiler('vala').find_library('libsodium', dirs: vapi_dir),
+    meson.get_compiler('c').find_library('sodium'),
 ]
 
 sources = files('Ppub.vala')
@@ -14,6 +16,24 @@ sources += files('Builder.vala')
 sources += files('StreamMonitor.vala')
 sources += files('Ppvm.vala')
 sources += files('Ppix.vala')
+sources += files('Ppcl.vala')
+
+sources += files('Pprf/Message.vala')
+sources += files('Pprf/MessageFactory.vala')
+sources += files('Pprf/CollectionMessage.vala')
+sources += files('Pprf/EncryptedMessage.vala')
+sources += files('Pprf/SignedMessage.vala')
+sources += files('Pprf/BeginUpload.vala')
+sources += files('Pprf/Failure.vala')
+sources += files('Pprf/GetAsset.vala')
+sources += files('Pprf/GetListing.vala')
+sources += files('Pprf/GetPublicationInformation.vala')
+sources += files('Pprf/GetPublication.vala')
+sources += files('Pprf/IdentifyServer.vala')
+sources += files('Pprf/Publish.vala')
+sources += files('Pprf/RegisterName.vala')
+sources += files('Pprf/UpdateCollection.vala')
+sources += files('Pprf/Upload.vala')
 
 ppub = shared_library('libppub', sources,
     name_prefix: '',

+ 2 - 1
src/tools/meson.build

@@ -1,3 +1,4 @@
 subdir('extract')
 subdir('viewer')
-subdir('create')
+subdir('create')
+subdir('ppcl')

+ 73 - 0
src/tools/ppcl/Ppcl.vala

@@ -0,0 +1,73 @@
+
+
+public static int main(string[] args) {
+
+    if(args.length < 2) {
+        print(@"USAGE: $(args[0]) [COMMAND]\nCommands:\n\tnew [output]: create new PPCL and private key\n\tnew-member [member name]: generates a new keypair for a member\n\tadd-member [collection] [member name] [member key]: add a member to the member register\n\tadd-domain [collection] [DNS name]: add an authorative domain\n\tpublish [collection] [publication] [member]: publish the specified PPUB to the collection\n");
+        return -1;
+    }
+
+    if(args[1] == "new") {
+        uint8[] signing_key;
+        var collection = new Ppub.Collection(out signing_key);
+        
+        var col_str = collection.to_string(signing_key);
+        FileUtils.set_contents(args[2], col_str, col_str.length);
+
+        var key_str = Base64.encode(signing_key);
+        FileUtils.set_contents(args[2] + ".private.key", key_str, key_str.length);
+        return 0;
+    }
+
+    if(args[1] == "new-member") {
+        uint8[] sk;
+        uint8[] pk;
+        Ppub.CollectionMember.new_keypair(out sk, out pk);
+        
+        var sk_str = Base64.encode(sk);
+        FileUtils.set_contents(args[2] + ".private.key", sk_str, sk_str.length);
+
+        var pk_str = Base64.encode(pk);
+        FileUtils.set_contents(args[2] + ".public.key", pk_str, pk_str.length);
+        return 0;
+    }
+
+    var collection_name = args[2];
+    string key_str;
+    FileUtils.get_contents(args[2] + ".private.key", out key_str, null);
+    var signing_key = Base64.decode(key_str);
+
+    var collection_stream = new DataInputStream(File.new_for_path(collection_name).read());
+    var collection = new Ppub.Collection.from_stream(collection_stream);
+
+    if(args[1] == "add-member") {
+        collection.increment_serial();
+        collection.members.add(new Ppub.CollectionMember(args[3], Base64.decode(args[4])));
+    }
+
+    else if(args[1] == "add-domain") {
+        collection.increment_serial();
+        collection.domains.add(args[3]);
+    }
+
+    else if(args[1] == "publish") {
+        var checksum = Ppub.CollectionPublication.generate_ppub_checksum(File.new_for_path(args[3]));
+        
+        string mem_key_str;
+        FileUtils.get_contents(args[4] + ".private.key", out mem_key_str, null);
+        var mem_signing_key = Base64.decode(mem_key_str);
+
+        var publication = new Ppub.CollectionPublication(args[3], new DateTime.now_local(), args[4], checksum, mem_signing_key);
+        collection.publications.add(publication);
+    }
+    else {
+        print(@"Unrecognised command $(args[1])\n");
+        return -2;
+    }
+
+    collection.touch();
+    var col_str = collection.to_string(signing_key);
+    FileUtils.set_contents(args[2], col_str, col_str.length);
+
+    return 0;
+}

+ 7 - 0
src/tools/ppcl/meson.build

@@ -0,0 +1,7 @@
+
+sources = files('Ppcl.vala')
+
+deps = dependencies
+deps += ppub_dep
+
+executable('ppcl', sources, dependencies: deps, install: true)

+ 237 - 0
src/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;
+         }
+         
+     }
+ 
+   }
+   
+ 
+ }