Sign in

    moonpgp

    Pure-MoonBit OpenPGP implementation (RFC 4880/9580): email encryption, decryption, signing and verification, interoperable with GnuPG and gopenpgp.

    openpgp
    pgp
    encryption
    signature
    email
    crypto
    imap
    smtp
    mime
    Download zip
    Version
    0.2.0
    License
    Apache-2.0
    Last updated
    1 hour ago
    Downloads
    250

    #moonpgp

    An OpenPGP library written in MoonBit for message encryption and decryption, signatures, key management, and PGP/MIME email workflows.

    Module: justinwongcn/moonpgp · Package documentation

    #Install

    Requires the MoonBit toolchain. Dependencies are resolved by moon.

    moon add justinwongcn/moonpgp

    Import the root package for the high-level API. Import mime and mail separately when needed:

    import { "justinwongcn/moonpgp", "justinwongcn/moonpgp/mime", "justinwongcn/moonpgp/mail", }

    The root package exposes the @api functions and their option types as @moonpgp.*. The mime and mail packages provide PGP/MIME parsing and RFC 5322 message composition.

    To try the command-line demo from a clone:

    git clone https://github.com/justinwongcn/moonpgp cd moonpgp moon run cmd/main -- help

    #Features

    • Keys: import, export, generate, lock, and unlock OpenPGP keys. Supports RSA, Ed25519 (EdDSA legacy), and Curve25519/X25519 workflows, with v4 keys as the default generation format and v6 support available through the relevant APIs. Key lifecycle helpers check capabilities, expiration, and revocation.
    • Messages: public-key and passphrase encryption; detached, inline, and cleartext signatures; session-key operations; optional ZIP/ZLIB compression. AES is used for generated messages; CAST5 and 3DES are supported for decryption only.
    • Modern formats: v6 keys and messages, including v6 PKESK/SKESK and v2 SEIPD with OCB, EAX, or GCM AEAD. Standard v4 recipient and passphrase workflows use v1 SEIPD by default.
    • Passphrase protection: iterated-and-salted S2K and Argon2 S2K, including supported AEAD-protected secret keys.
    • Email: mime parses and verifies PGP/MIME messages; mail builds RFC 5322 messages, including signed and encrypted PGP/MIME. SMTP/IMAP clients are examples under example/, not part of the library API, and support native only.

    #Interoperability and checks

    The test suite includes selected interoperability workflows with GnuPG, OpenSSL, and go-crypto, along with RFC test vectors. See scripts/interop_check.sh for the GnuPG interoperability checks.

    Local checks:

    moon check --target all --deny-warn moon build --target all moon test --target native moon fmt --check

    CI builds and tests the native, wasm, and js targets. Native is the preferred target. Operations that generate keys or random values require a platform entropy source; availability depends on the runtime.

    #Security and limitations

    • Import validates the supported key signatures, including User ID certifications, subkey bindings, and v6 direct-key signatures. This is not identity or third-party trust validation; applications must establish trust in a key separately.
    • Ed25519 and X25519 field arithmetic uses @bigint and is not constant-time. Garbage-collected runtimes also cannot guarantee private-key memory wiping.
    • Decryption of untrusted Argon2-protected data is subject to a default 256 MiB memory limit. Requests above the limit are rejected before allocation; callers can choose a different limit where the API allows it.
    • v2 SEIPD messages are not padded by the encryption APIs and therefore reveal approximate plaintext length.
    • On targets without a platform clock, expiration checks are disabled and revocation dates cannot be evaluated; revocation signatures are still cryptographically checked. The example SMTP/IMAP transports are native-only.

    See THIRD-PARTY-NOTICES.md for dependency and attribution details.

    #License

    Apache-2.0. See LICENSE, NOTICE, and THIRD-PARTY-NOTICES.md.

    AeadMode

    Option types that appear in @api signatures, re-exported so the bare root import covers the whole main workflow (an AeadMode for encrypt_with_password_v6, a CompressedAlgo for the encryption builders, a CipherAlgo for session keys, a HashId for sign_cleartext_with).

    CipherAlgo

    Symmetric cipher algorithms, RFC 9580 §9.3 numbers. TripleDES (2) and CAST5 (3) exist for the read side only — RFC 4880 §9.2 makes 3DES a MUST-implement and CAST5 was GnuPG's pre-AES default; the generation paths always pick AES.

    CompressedAlgo

    Compression algorithms for the Compressed Data packet (RFC 9580 §9.4).

    DecryptResult

    Root facade of justinwongcn/moonpgp: a single import "justinwongcn/moonpgp" gives the complete high-level OpenPGP workflow (@moonpgp.generate_key, @moonpgp.encrypt_with_password, sign/verify, builders, key management). Every item is re-exported from its owning public package, so methods resolve through the usual owner loading and lower layers (@packet, @pki, @armor, ...) remain directly importable for advanced use.

    DecryptionBuilder

    Root facade of justinwongcn/moonpgp: a single import "justinwongcn/moonpgp" gives the complete high-level OpenPGP workflow (@moonpgp.generate_key, @moonpgp.encrypt_with_password, sign/verify, builders, key management). Every item is re-exported from its owning public package, so methods resolve through the usual owner loading and lower layers (@packet, @pki, @armor, ...) remain directly importable for advanced use.

    EncryptionBuilder

    Root facade of justinwongcn/moonpgp: a single import "justinwongcn/moonpgp" gives the complete high-level OpenPGP workflow (@moonpgp.generate_key, @moonpgp.encrypt_with_password, sign/verify, builders, key management). Every item is re-exported from its owning public package, so methods resolve through the usual owner loading and lower layers (@packet, @pki, @armor, ...) remain directly importable for advanced use.

    HashId

    Hash algorithms used by OpenPGP, identified by their RFC 9580 §9.5 algorithm numbers. This is the PGP-level identity; concrete digest computation is delegated to moonbitlang/x/crypto inside this package only, so the hash backend can be swapped without touching callers.

    KeyGenBuilder

    Root facade of justinwongcn/moonpgp: a single import "justinwongcn/moonpgp" gives the complete high-level OpenPGP workflow (@moonpgp.generate_key, @moonpgp.encrypt_with_password, sign/verify, builders, key management). Every item is re-exported from its owning public package, so methods resolve through the usual owner loading and lower layers (@packet, @pki, @armor, ...) remain directly importable for advanced use.

    KeyRing

    Root facade of justinwongcn/moonpgp: a single import "justinwongcn/moonpgp" gives the complete high-level OpenPGP workflow (@moonpgp.generate_key, @moonpgp.encrypt_with_password, sign/verify, builders, key management). Every item is re-exported from its owning public package, so methods resolve through the usual owner loading and lower layers (@packet, @pki, @armor, ...) remain directly importable for advanced use.

    Pgp

    Root facade of justinwongcn/moonpgp: a single import "justinwongcn/moonpgp" gives the complete high-level OpenPGP workflow (@moonpgp.generate_key, @moonpgp.encrypt_with_password, sign/verify, builders, key management). Every item is re-exported from its owning public package, so methods resolve through the usual owner loading and lower layers (@packet, @pki, @armor, ...) remain directly importable for advanced use.

    PrivateKey

    Root facade of justinwongcn/moonpgp: a single import "justinwongcn/moonpgp" gives the complete high-level OpenPGP workflow (@moonpgp.generate_key, @moonpgp.encrypt_with_password, sign/verify, builders, key management). Every item is re-exported from its owning public package, so methods resolve through the usual owner loading and lower layers (@packet, @pki, @armor, ...) remain directly importable for advanced use.

    PublicKey

    Root facade of justinwongcn/moonpgp: a single import "justinwongcn/moonpgp" gives the complete high-level OpenPGP workflow (@moonpgp.generate_key, @moonpgp.encrypt_with_password, sign/verify, builders, key management). Every item is re-exported from its owning public package, so methods resolve through the usual owner loading and lower layers (@packet, @pki, @armor, ...) remain directly importable for advanced use.

    SecretSubkeyCert

    Root facade of justinwongcn/moonpgp: a single import "justinwongcn/moonpgp" gives the complete high-level OpenPGP workflow (@moonpgp.generate_key, @moonpgp.encrypt_with_password, sign/verify, builders, key management). Every item is re-exported from its owning public package, so methods resolve through the usual owner loading and lower layers (@packet, @pki, @armor, ...) remain directly importable for advanced use.

    SessionKey

    Root facade of justinwongcn/moonpgp: a single import "justinwongcn/moonpgp" gives the complete high-level OpenPGP workflow (@moonpgp.generate_key, @moonpgp.encrypt_with_password, sign/verify, builders, key management). Every item is re-exported from its owning public package, so methods resolve through the usual owner loading and lower layers (@packet, @pki, @armor, ...) remain directly importable for advanced use.

    SubkeyCert

    Root facade of justinwongcn/moonpgp: a single import "justinwongcn/moonpgp" gives the complete high-level OpenPGP workflow (@moonpgp.generate_key, @moonpgp.encrypt_with_password, sign/verify, builders, key management). Every item is re-exported from its owning public package, so methods resolve through the usual owner loading and lower layers (@packet, @pki, @armor, ...) remain directly importable for advanced use.

    UserIdBinding

    Root facade of justinwongcn/moonpgp: a single import "justinwongcn/moonpgp" gives the complete high-level OpenPGP workflow (@moonpgp.generate_key, @moonpgp.encrypt_with_password, sign/verify, builders, key management). Every item is re-exported from its owning public package, so methods resolve through the usual owner loading and lower layers (@packet, @pki, @armor, ...) remain directly importable for advanced use.

    VerifyStatus

    Root facade of justinwongcn/moonpgp: a single import "justinwongcn/moonpgp" gives the complete high-level OpenPGP workflow (@moonpgp.generate_key, @moonpgp.encrypt_with_password, sign/verify, builders, key management). Every item is re-exported from its owning public package, so methods resolve through the usual owner loading and lower layers (@packet, @pki, @armor, ...) remain directly importable for advanced use.

    DEFAULT_MAX_DECOMPRESSED_BYTES

    let DEFAULT_MAX_DECOMPRESSED_BYTES : Int

    Default ceiling on decompressed payload size (M4): a 512 MiB bound on what an untrusted compressed packet may expand to. Mirrors the Argon2 pre-allocation cap policy; override per call with max_decompressed~.

    decrypt_and_verify

    fn decrypt_and_verify(message : String, decryption_keys : Array[
    PrivateKey
    ], verifier :
    PublicKey
    ?, max_decompressed? : Int) -> (Bytes,
    VerifyStatus
    ) raise

    Decrypt an armored message (password OR private keys) and verify an inline signature against verifier. Returns (data, status).

    decrypt_binary_with_password

    fn decrypt_binary_with_password(data : BytesView, passphrase : String, argon2_max_memory_kib? : Int, max_decompressed? : Int) -> Bytes raise

    Same as decrypt_with_password but takes binary packet data.

    Every SKESK in the stream is tried until one both unwraps AND opens the message payload (N4): a v4 SKESK has no integrity check of its own, so a wrong passphrase can unwrap to structurally plausible garbage whose wrongness only surfaces at the SEIPD MDC — the retry must therefore span the payload decryption, not just the unwrap.

    decrypt_binary_with_private_key

    fn decrypt_binary_with_private_key(data : BytesView, key :
    PrivateKey
    , max_decompressed? : Int) -> Bytes raise

    Decrypt a binary message with an unlocked private key. Matches PKESKs against the key's ids (RSA or ECDH), recovers the session key, checks the session-key checksum, and unwraps the SEIPD payload.

    decrypt_to_session_key

    fn decrypt_to_session_key(message : String, sk :
    SessionKey
    ) -> Bytes raise

    Decrypt a message with a caller-held session key. The message may be a bare SEIPD container (from encrypt_to_session_key) or a normal PKESK/SKESK-protected message whose session key the caller knows.

    decrypt_with_password

    fn decrypt_with_password(message : String, passphrase : String, argon2_max_memory_kib? : Int, max_decompressed? : Int) -> Bytes raise

    Decrypt an (optionally armored) message using a passphrase. Returns the literal data. Compressed packets are transparently decompressed.

    decrypt_with_private_key

    fn decrypt_with_private_key(message : String, key :
    PrivateKey
    , max_decompressed? : Int) -> Bytes raise

    Decrypt an armored message with an unlocked private key.

    encrypt_to_recipients

    fn encrypt_to_recipients(plaintext : BytesView, recipients : Array[
    PublicKey
    ], file_name? : String, compress? :
    CompressedAlgo
    ) -> String raise

    Encrypt plaintext to one or more recipients' public keys. Each recipient gets its own fresh PKESK encoding of the session key (RFC 9580 §5.1.3 requires re-encoding per key). Result: armored PGP MESSAGE of PKESK(s) + SEIPD, v2 for v6 recipients and v1 otherwise.

    encrypt_to_session_key

    fn encrypt_to_session_key(plaintext : BytesView, sk :
    SessionKey
    , file_name? : String, compress? :
    CompressedAlgo
    ) -> String raise

    Encrypt plaintext under a caller-held session key: a bare SEIPD v1 container with no PKESK/SKESK — only whoever holds sk out of band can read it (the gopenpgp "raw session key" backend).

    encrypt_with_password

    fn encrypt_with_password(plaintext : BytesView, passphrase : String, file_name? : String, aead? : Bool, argon2? : Bool, argon2_memory_exp? : Int, compress? :
    CompressedAlgo
    ) -> String raise

    Encrypt plaintext to a passphrase. Produces an armored PGP MESSAGE with a v4 SKESK (iterated+salted S2K, AES-256 session key wrapped in CFB) followed by a v1 SEIPD packet holding the literal data.

    encrypt_with_password_argon2

    fn encrypt_with_password_argon2(plaintext : BytesView, passphrase : String, file_name? : String, passes? : Int, parallelism? : Int, memory_exp? : Int, compress? :
    CompressedAlgo
    ) -> String raise

    Encrypt plaintext to a passphrase using Argon2id as the S2K specifier (type 4, RFC 9580 §3.7.1.4) — the RFC's recommendation, and the shape of Appendix A.12. The message keeps the v4 SKESK form (CFB with an all-zero IV) plus a v1 SEIPD packet, so every reader that understands Argon2 can consume it.

    memory_exp is the exponent of the Argon2 memory size in KiB (default 16 → 64 MiB, go-crypto's default); passes and parallelism default to t=3, p=4.

    encrypt_with_password_v6

    fn encrypt_with_password_v6(plaintext : BytesView, passphrase : String, file_name? : String, aead? :
    AeadMode
    , argon2? : Bool, passes? : Int, parallelism? : Int, memory_exp? : Int, compress? :
    CompressedAlgo
    ) -> String raise

    Encrypt plaintext to a passphrase in the RFC 9580 v6 shape: a v6 SKESK (AEAD + HKDF-SHA256 key separation, §5.3.2) followed by a v2 SEIPD packet (§5.13.2). argon2 (default true) selects the Argon2id S2K the RFC recommends for v6; set it to false for the much cheaper iterated+salted SHA-256.

    generate_ed25519_key

    fn generate_ed25519_key(user_id : String) ->
    PrivateKey
    raise

    Generate a transferable key with an Ed25519 primary (EdDSA legacy, signing/certification) and a Curve25519Legacy ECDH encryption subkey — the gopenpgp "Default" profile shape. EdDSA signatures require the digest itself as the Ed25519 message (RFC 9580 §5.2.3).

    generate_ed25519_v6_key

    fn generate_ed25519_v6_key(user_id : String) ->
    PrivateKey
    raise

    Generate a v6 Ed25519 signing key with an X25519 encryption subkey (RFC 9580 §5.5.5.7/§5.5.5.9) — the shape of the Appendix A.3/A.4 sample certificate: a v6 primary key with a User ID self-certification, the Direct Key signature the RFC requires before a v6 key may be used, and a v6 X25519 subkey with a v6 binding signature.

    generate_key

    fn generate_key(user_id : String, bits? : Int) ->
    PrivateKey
    raise

    Generate a fresh transferable key: RSA primary (certify+sign) with a User ID self-certification plus an RSA encryption subkey.

    generate_session_key

    Generate a fresh random session key (platform entropy; AES-256 unless another cipher is requested).

    pgp

    gopenpgp-style entry point: pgp().encryption()....

    recover_session_key_with_key

    fn recover_session_key_with_key(message : String, key :
    PrivateKey
    ) ->
    SessionKey
    raise

    Recover the session key of a message encrypted to key's public key: unwrap the matching PKESK without decrypting the payload. The algorithm id comes from the PKESK payload for the v4 shape and from the SEIPD v2 header for v6.

    recover_session_key_with_password

    fn recover_session_key_with_password(message : String, passphrase : String, argon2_max_memory_kib? : Int) ->
    SessionKey
    raise

    Recover the session key of a password-encrypted message (the gopenpgp RetrieveSessionKey flow): unwrap the SKESK without touching the payload.

    sign_and_encrypt

    fn sign_and_encrypt(data : BytesView, signer :
    PrivateKey
    , recipients : Array[
    PublicKey
    ], text_mode? : Bool, compress? :
    CompressedAlgo
    , created? : Int, file_name? : String) -> String raise

    Sign and encrypt in one step (inline signature inside SEIPD).

    created is the signature creation time in seconds since 1970; without it the current time is used (see sign_detached). file_name names the literal packet (N9; default "message.txt").

    sign_cleartext

    fn sign_cleartext(text : String, key :
    PrivateKey
    , created? : Int) -> String raise

    Create a cleartext signed message (RFC 9580 §7) over UTF-8 text with the default SHA-256 signature of the primary key.

    created is the signature creation time in seconds since 1970; without it the current time is used (see sign_detached).

    sign_cleartext_with

    fn sign_cleartext_with(text : String, key :
    PrivateKey
    , hash :
    HashId
    , created? : Int) -> String raise

    Create a cleartext signed message with an explicit hash algorithm.

    sign_detached

    fn sign_detached(data : BytesView, key :
    PrivateKey
    , text_mode? : Bool, created? : Int) -> String raise

    Create a detached ASCII-armored signature over data.

    created is the signature creation time in seconds since 1970; without it the current time is used. Passing the time makes the signature reproducible for RSA PKCS#1 v1.5 and Ed25519 keys, which sign deterministically (for example for reproducible builds).

    sign_inline

    fn sign_inline(data : BytesView, key :
    PrivateKey
    , text_mode? : Bool, created? : Int, file_name? : String) -> String raise

    Create an inline-signed, armored message: One-Pass-Signature || Literal || Signature.

    created is the signature creation time in seconds since 1970; without it the current time is used (see sign_detached — deterministic for RSA/Ed25519 keys, so the message becomes reproducible). file_name names the literal packet (N9; default "message.txt").

    verify_cleartext

    fn verify_cleartext(message : String, verifier :
    PublicKey
    ) -> (String,
    VerifyStatus
    ) raise

    Verify a cleartext signed message. Returns the de-escaped, canonical cleartext and the verification status against verifier.

    verify_detached

    fn verify_detached(data : BytesView, armored_signature : String, verifier :
    PublicKey
    ) ->
    VerifyStatus
    raise

    Verify a detached armored signature over data using verifier.

    verify_inline

    fn verify_inline(message : String, verifier :
    PublicKey
    , max_decompressed? : Int) -> (Bytes,
    VerifyStatus
    ) raise

    Verify an inline-signed armored message; returns the literal data and the verification status against verifier.

    verify_key_signature

    Verify a self-certification or binding signature over key material. Deliberately public (F14 review decision): it is the key-management entry point for validating third-party keys, exercised from outside the package by the gopenpgp self-certification cross-validation tests.

    Source Files