# GPGMEx - Native Elixir bindings for GnuPG # Copyright (C) 2022 Matt Silbernagel # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . defmodule GPG.NIF do @moduledoc false @behaviour GPG.NativeAPI @gpg_bin Application.compile_env(:gpgmex, :gpg_bin, "/usr/bin/gpg") @gpg_home Application.compile_env(:gpgmex, :gpg_home, "~/.gnupg") use GPG.ZigSupport ~z""" const c = @cImport({ @cInclude("gpgme.h"); @cInclude("locale.h"); @cDefine("SIZE", "4092"); }); /// nif: check_version/0 fn check_version(env: beam.env) beam.term { var y = c.gpgme_check_version(null); return beam.make_cstring_charlist(env, y); } /// nif: check_openpgp_supported/0 fn check_openpgp_supported() bool { var err = c.gpgme_engine_check_version(c.GPGME_PROTOCOL_OpenPGP); if (err != c.GPG_ERR_NO_ERROR) { return false; } return true; } /// resource: engine_info_struct definition const engine_info_struct = struct { filename: [*c]u8, homedir: [*c]u8, }; /// resource: context_struct definition const context_struct = struct { context: c.gpgme_ctx_t }; /// nif: engine_info/0 fn engine_info(env: beam.env) beam.term { var enginfo: c.gpgme_engine_info_t = undefined; const err = c.gpgme_get_engine_info(&enginfo); if (err != c.GPG_ERR_NO_ERROR) { return 9888; } var resource_filename = beam.allocator.alloc(u8, 64) catch return beam.raise_enomem(env); errdefer beam.allocator.free(resource_filename); var resource_homedir = beam.allocator.alloc(u8, 64) catch return beam.raise_enomem(env); errdefer beam.allocator.free(resource_homedir); return __resource__.create(engine_info_struct, env, .{ .filename = enginfo.*.file_name, .homedir = enginfo.*.home_dir, }) catch return beam.raise_resource_error(env); } /// nif: get_filename/1 fn get_filename(env: beam.env, res: beam.term) beam.term { var result = __resource__.fetch(engine_info_struct, env, res) catch return beam.raise_resource_error(env); return beam.make_cstring_charlist(env, result.filename); } /// nif: get_homedir/1 fn get_homedir(env: beam.env, res: beam.term) beam.term { var result = __resource__.fetch(engine_info_struct, env, res) catch return beam.raise_resource_error(env); return beam.make_cstring_charlist(env, result.homedir); } /// nif: create_context/0 fn create_context(env: beam.env) beam.term { var ceofcontext: c.gpgme_ctx_t = undefined; var err = c.gpgme_new(&ceofcontext); if (err != c.GPG_ERR_NO_ERROR) { std.log.err("ERROR {}", .{err}); return beam.raise_resource_error(env); } err = c.gpgme_set_protocol(ceofcontext, c.GPGME_PROTOCOL_OpenPGP); if (err != c.GPG_ERR_NO_ERROR) { return beam.raise_resource_error(env); } // set engine info in our context err = c.gpgme_ctx_set_engine_info(ceofcontext, c.GPGME_PROTOCOL_OpenPGP, "#{@gpg_bin}", "#{@gpg_home}"); if (err != c.GPG_ERR_NO_ERROR) { std.log.err("ERROR {}", .{err}); return beam.raise_resource_error(env); } _ = c.gpgme_set_armor(ceofcontext, 1); var resource_context = beam.allocator.alloc(c.gpgme_engine_info_t, 64) catch return beam.raise_enomem(env); errdefer beam.allocator.free(resource_context); return __resource__.create(context_struct, env, .{ .context = ceofcontext }) catch return beam.raise_resource_error(env); } /// Encrypt data /// nif: encrypt/3 fn encrypt(env: beam.env, res: beam.term, email: []u8, text: []u8) beam.term { var context = __resource__.fetch(context_struct, env, res) catch return beam.raise_resource_error(env); var key: c.gpgme_key_t = undefined; var err = c.gpgme_op_keylist_start(context.context, email.ptr, 0); while (err == c.GPG_ERR_NO_ERROR) { err = c.gpgme_op_keylist_next(context.context, &key); if (err != c.GPG_ERR_NO_ERROR) { _ = c.gpgme_op_keylist_end(context.context); c.gpgme_key_release(key); c.gpgme_release(context.context); return beam.make_error_binary(env, "error retrieving key"); } var cipher: c.gpgme_data_t = undefined; err = c.gpgme_data_new(&cipher); var to_encrypt: c.gpgme_data_t = undefined; err = c.gpgme_data_new_from_mem(&to_encrypt, text.ptr, text.len, 0); var keys = [_]c.gpgme_key_t{ key, null }; err = c.gpgme_op_encrypt(context.context, &keys, c.GPGME_ENCRYPT_ALWAYS_TRUST, to_encrypt, cipher); if (err != c.GPG_ERR_NO_ERROR) { _ = c.gpgme_op_keylist_end(context.context); c.gpgme_data_release(cipher); c.gpgme_key_release(key); c.gpgme_release(context.context); return beam.make_error_binary(env, "error encrypting the data"); } // READ THE ENCRYPTED DATA var d: [c.SIZE]u8 = undefined; _ = c.gpgme_data_seek(cipher, 0, c.SEEK_SET); var read_new_bytes_2 = c.gpgme_data_read(cipher, &d, c.SIZE); while (read_new_bytes_2 > 0) { read_new_bytes_2 = c.gpgme_data_read(cipher, &d, c.SIZE); } _ = c.gpgme_op_keylist_end(context.context); c.gpgme_data_release(to_encrypt); c.gpgme_data_release(cipher); c.gpgme_key_release(key); c.gpgme_release(context.context); const buf_slice = d[0..]; return beam.make_ok_term(env, beam.make_cstring_charlist(env, buf_slice)); } return beam.make_error_binary(env, "key not found"); } /// Decrypt data /// nif: decrypt/2 fn decrypt(env: beam.env, res: beam.term, data: []u8) beam.term { var context = __resource__.fetch(context_struct, env, res) catch return beam.raise_resource_error(env); var cipher: c.gpgme_data_t = undefined; var err = c.gpgme_data_new_from_mem(&cipher, data.ptr, data.len, 0); var decrypted: c.gpgme_data_t = undefined; err = c.gpgme_data_new(&decrypted); if (err != c.GPG_ERR_NO_ERROR) { return beam.make_error_binary(env, "unable to build data object"); } err = c.gpgme_op_decrypt(context.context, cipher, decrypted); if (err != c.GPG_ERR_NO_ERROR) { return beam.make_error_binary(env, "unable to decrypt cipher"); } // READ THE ENCRYPTED DATA var d: [c.SIZE]u8 = undefined; _ = c.gpgme_data_seek(decrypted, 0, c.SEEK_SET); var read_new_bytes_2 = c.gpgme_data_read(decrypted, &d, c.SIZE); while (read_new_bytes_2 > 0) { read_new_bytes_2 = c.gpgme_data_read(decrypted, &d, c.SIZE); } // RELEASE THE POINTERS c.gpgme_data_release(decrypted); c.gpgme_data_release(cipher); const buf_slice = d[0..]; return beam.make_ok_term(env, beam.make_cstring_charlist(env, buf_slice)); } /// Get the public key for a particular email /// nif: public_key/2 fn public_key(env: beam.env, res: beam.term, email: beam.term) beam.term { var context = __resource__.fetch(context_struct, env, res) catch return beam.raise_resource_error(env); var data: c.gpgme_data_t = undefined; var err = c.gpgme_data_new(&data); if (err != c.GPG_ERR_NO_ERROR) { return beam.raise_resource_error(env); } err = c.gpgme_data_set_encoding(data, c.GPGME_DATA_ENCODING_ARMOR); if (err != c.GPG_ERR_NO_ERROR) { return beam.raise_resource_error(env); } // EXPORT AND GET PUBLIC KEY err = c.gpgme_op_export(context.context, email, 0, data); if (err != c.GPG_ERR_NO_ERROR) { return beam.raise_resource_error(env); } var read_bytes = c.gpgme_data_seek(data, 0, c.SEEK_END); if (read_bytes == -1) { std.log.err("data-seek-err: {}", .{12}); return beam.raise_resource_error(env); } read_bytes = c.gpgme_data_seek(data, 0, c.SEEK_SET); if (read_bytes == -1) { return beam.raise_resource_error(env); } read_bytes = c.gpgme_data_seek(data, 0, c.SEEK_SET); var buf: [c.SIZE]u8 = undefined; var read_new_bytes = c.gpgme_data_read(data, &buf, c.SIZE); while (read_new_bytes > 0) { read_new_bytes = c.gpgme_data_read(data, &buf, c.SIZE); } const buf_slice = buf[0..]; // RELEASE THE POINTERS c.gpgme_data_release(data); return beam.make_cstring_charlist(env, buf_slice); } /// nif: generate_key/2 fn generate_key(env: beam.env, res: beam.term, email: []u8) c_ulong { var context = __resource__.fetch(context_struct, env, res) catch return beam.raise_resource_error(env); var err = c.gpgme_op_createkey(context.context, email.ptr, null, 0, 0, null, c.GPGME_CREATE_CERT & c.GPGME_CREATE_NOEXPIRE & c.GPGME_CREATE_ENCR); if (err != c.GPG_ERR_NO_ERROR) { if (err == c.GPG_ERR_NOT_SUPPORTED) { return beam.make_error_term(env, 190); } std.log.err("ERROR {}", .{err}); return beam.raise_resource_error(env); } return 0; } /// nif: delete_key/2 fn delete_key(env: beam.env, res: beam.term, email: []u8) c_ulong { var context = __resource__.fetch(context_struct, env, res) catch return beam.raise_resource_error(env); var key: c.gpgme_key_t = undefined; var err = c.gpgme_op_keylist_start(context.context, email.ptr, 0); while (err == c.GPG_ERR_NO_ERROR) { err = c.gpgme_op_keylist_next(context.context, &key); if (err != c.GPG_ERR_NO_ERROR) { // RELEASE THE POINTERS c.gpgme_key_release(key); break; } err = c.gpgme_op_delete_ext(context.context, key, c.GPGME_DELETE_ALLOW_SECRET | c.GPGME_DELETE_FORCE); if (err != c.GPG_ERR_NO_ERROR) { c.gpgme_key_release(key); break; } c.gpgme_key_release(key); return 0; } c.gpgme_key_release(key); return 1; } /// nif: import_key/2 fn import_key(env: beam.env, res: beam.term, data: []u8) beam.term { var context = __resource__.fetch(context_struct, env, res) catch return beam.raise_resource_error(env); var cipher: c.gpgme_data_t = undefined; var err = c.gpgme_data_new_from_mem(&cipher, data.ptr, data.len, 0); if (err != c.GPG_ERR_NO_ERROR) { return beam.make_error_binary(env, "invalid key"); } err = c.gpgme_op_import(context.context, cipher); if (err != c.GPG_ERR_NO_ERROR) { return beam.make_error_binary(env, "unable to import the key"); } return beam.make_ok(env); } """ end