git » sdk » main » tree

[main] / borogove / XEP0454.hx

package borogove;

import haxe.io.Bytes;
import thenshim.Promise;

using StringTools;

function fetch(aesgcm: AesGcm): Promise<Bytes> {
	// Fetch all data into memory because AES-GCM APIs really want to verify
	// everything before giving access to plaintext.
	// TODO: This means we ought to refuse to download if the size is too big:
	// https://github.com/haxetink/tink_http/issues/159
	return new Promise((resolve, reject) -> {
		tink.http.Client.fetch(aesgcm.https).all().handle((o) -> switch o {
			case Success(res):
				resolve(res.body);
			case Failure(e):
				reject(e);
		});
	}).then((encrypted) -> {
		#if js
		final subtle: js.html.SubtleCrypto = untyped globalThis.crypto.subtle;
		return (subtle.importKey("raw", aesgcm.key.getData(), "AES-GCM", false, ["decrypt"]).then(key ->
			subtle.decrypt({ name: "AES-GCM", iv: aesgcm.iv.getData() }, key, encrypted.toBytes().getData())
		).then(decrypted -> Bytes.ofData(decrypted)) : Promise<Bytes>);
		#else
		final aes = new haxe.crypto.Aes(aesgcm.key, aesgcm.iv);
		return aes.decrypt(haxe.crypto.mode.Mode.GCM, encrypted, Bytes.alloc(0));
		#end
	});
}

function put(source: tink.io.Source.RealSource, httpPut: (tink.io.Source.RealSource, Int)->Promise<String>): Promise<{ uri: String, size: Int }> {
	final iv = haxe.crypto.random.SecureRandom.bytes(12);
	final key = haxe.crypto.random.SecureRandom.bytes(32);

	return new Promise((resolve, reject) -> {
		tink.io.Source.RealSourceTools.all(source).handle(o -> switch o {
			case Success(bytes): resolve((bytes : Bytes));
			case Failure(e): reject(e);
		});
	}).then(bytes -> {
		#if js
		final subtle: js.html.SubtleCrypto = untyped globalThis.crypto.subtle;
		final encryptedP: Promise<Bytes> = subtle.importKey("raw", key.getData(), "AES-GCM", false, ["encrypt"]).then(key ->
			subtle.encrypt({ name: "AES-GCM", iv: iv.getData() }, key, bytes.getData())
		).then(encrypted -> Bytes.ofData(encrypted));
		#else
		final aes = new haxe.crypto.Aes(key, iv);
		final encryptedP = Promise.resolve(aes.encrypt(haxe.crypto.mode.Mode.GCM, bytes, Bytes.alloc(0), 16));
		#end
		return encryptedP.then(encrypted -> httpPut(encrypted, encrypted.length).then(uri -> ({ uri: ~/^https?:\/\//.map(uri, _ -> "aesgcm://") + "#" + iv.toHex() + key.toHex(), size: encrypted.length })));
	});
}

typedef AesGcm = {
	https: String,
	iv: haxe.io.Bytes,
	key: haxe.io.Bytes,
	mime: String
}

function parse(uriStr: String) : Null<AesGcm> {
	final uri = parseURI(uriStr);
	if (uri == null) return null;

	final https = "https:" + uri.rest;
	final ext = haxe.io.Path.extension(uri.rest);
	final iv = Bytes.ofHex(uri.fragment.substr(0, 24));
	final key = Bytes.ofHex(uri.fragment.substr(24));

	// There is no info about mime with XEP0454 so infer from extension...
	final mime = switch (ext) {
	case "jpg": "image/jpeg";
	case "png": "image/png";
	default: "application/octet-stream";
	};

	return { https: https, iv: iv, key: key, mime: mime };
}

private typedef SplitURI = {
	rest: String,
	fragment: String
}

private function parseURI(uriStr: String) : Null<SplitURI> {
	var r = ~/^aesgcm:(\/\/[^#]+)#(.{88})$/;
	if (!r.match(uriStr)) return null;

	var frag = r.matched(2);
	return {
		rest     : r.matched(1),
		fragment : frag != null ? frag : ""
	};
}