Created
February 8, 2026 04:13
-
-
Save 5HT/cc464cf88af6ed1e14769fa43df32a5f to your computer and use it in GitHub Desktop.
v2.txt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| defmodule KDF do | |
| # Existing hash lengths | |
| def hl(:md5), do: 16 | |
| def hl(:sha), do: 20 | |
| def hl(:sha224), do: 28 | |
| def hl(:sha256), do: 32 | |
| def hl(:sha384), do: 48 | |
| def hl(:sha512), do: 64 | |
| # Existing Concat KDF (NIST SP 800-56A style, used in standard ECC CMS) | |
| def derive(hash, shared, len, info) do | |
| block_size = hl(hash) | |
| n = Float.ceil(len / block_size) |> round() | |
| Enum.reduce(1..n, <<>>, fn i, acc -> | |
| counter = <<i::32-big>> | |
| acc <> :crypto.hash(hash, shared <> counter <> info) | |
| end) | |
| |> :binary.part(0, len) | |
| end | |
| # NEW: HKDF (RFC 5869) – used by Signal, WhatsApp, and your Chat X.509 v1 | |
| def hkdf(hash_alg, ikm, info, len, salt \\ <<0::256>>) do | |
| prk = :crypto.mac(:hmac, hash_alg, salt, ikm) | |
| expand(hash_alg, prk, info, len) | |
| end | |
| defp expand(hash_alg, prk, info, len) do | |
| block_size = hl(hash_alg) | |
| n = Float.ceil(len / block_size) |> round() | |
| Enum.reduce(1..n, {<<>>, <<>>}, fn i, {prev, okm} -> | |
| input = prev <> info <> <<i::8>> | |
| t = :crypto.mac(:hmac, hash_alg, prk, input) | |
| {t, okm <> t} | |
| end) | |
| |> elem(1) | |
| |> :binary.part(0, len) | |
| end | |
| end | |
| # Updated ECC-CMS SharedInfo encoder – now parameterised with the algorithm OID | |
| # (key-wrap OID for wrapped mode, content-encryption OID for direct mode) | |
| def ecc_shared_info(ukm, key_bits, alg_oid) do | |
| supp_pub_info = <<key_bits::32-big>> | |
| :'CMSECCAlgs-2009-02'.encode( | |
| :'ECC-CMS-SharedInfo', | |
| {:'ECC-CMS-SharedInfo', | |
| {:'AlgorithmIdentifier', alg_oid, :asn1_NOVALUE}, | |
| ukm, | |
| supp_pub_info} | |
| ) | |
| |> elem(1) | |
| end | |
| # Mapping for standard KDF OIDs → hash algorithm | |
| def map(:'dhSinglePass-stdDH-sha512kdf-scheme'), do: :sha512 | |
| def map(:'dhSinglePass-stdDH-sha384kdf-scheme'), do: :sha384 | |
| def map(:'dhSinglePass-stdDH-sha256kdf-scheme'), do: :sha256 | |
| # NEW: Mapping for HKDF (use a custom OID in your implementation, e.g. a private OID) | |
| def map(:hkdf_sha256), do: {:hkdf, :sha256} | |
| # Modified KARI decryption – now supports BOTH wrapped (standard CMS) and direct derivation | |
| # (used by your Chat X.509 v1, Signal-style basic messages, WhatsApp-style basic messages) | |
| def kari(kari, private_key_bin, originator_alg_oid, content_enc_oid, key_wrap_oid \\ {2,16,840,1,101,3,4,1,45}, data, iv, key_bits \\ 256) do | |
| # Parse KARI (KeyAgreeRecipientInfo) | |
| {_, :v3, originator, ukm, kdf_field, recip_enc_keys} = kari | |
| # Extract ephemeral public key from originator | |
| {_, {_, _, ephemeral_pub}} = originator | |
| # Look up curve/scheme and KDF | |
| {curve_scheme, _} = CA.ALG.lookup(originator_alg_oid) | |
| {kdf_oid, _} = kdf_field | |
| kdf_mode = map(kdf_oid) || :sha256 # fallback to SHA-256 Concat KDF | |
| # Compute ECDH shared secret (supports NIST curves + X25519/X448) | |
| shared = | |
| if curve_scheme in [:x25519, :x448] do | |
| :crypto.compute_key(curve_scheme, ephemeral_pub, private_key_bin) | |
| else | |
| :crypto.compute_key(:ecdh, ephemeral_pub, private_key_bin, curve_scheme) | |
| end | |
| # Determine mode: wrapped (has encryptedKey) or direct (no encryptedKey) | |
| {mode, alg_oid_for_info} = | |
| case recip_enc_keys do | |
| [{_, _, encrypted_key}] when encrypted_key != <<>> -> | |
| {:wrapped, key_wrap_oid} # standard CMS wrapped mode | |
| [] -> | |
| {:direct, content_enc_oid} # direct derivation (Chat X.509 v1 / Signal-like) | |
| _ -> | |
| raise "Unsupported RecipientEncryptedKeys" | |
| end | |
| # Build OtherInfo (domain separation) | |
| payload = ecc_shared_info(ukm, key_bits, alg_oid_for_info) | |
| # Derive key | |
| derived = | |
| case kdf_mode do | |
| {:hkdf, hash} -> | |
| KDF.hkdf(hash, shared, payload, div(key_bits, 8)) | |
| hash when is_atom(hash) -> | |
| KDF.derive(hash, shared, div(key_bits, 8), payload) | |
| end | |
| # Decrypt | |
| case mode do | |
| :wrapped -> | |
| cek = CA.AES.KW.unwrap(hd(elem(recip_enc_keys, 0)) |> elem(2), derived) | |
| CA.AES.decrypt(content_enc_oid, data, cek, iv) | |
| :direct -> | |
| CA.AES.decrypt(content_enc_oid, data, derived, iv) | |
| end | |
| |> then(&{:ok, &1}) | |
| end | |
| # Simple P2P scheme for Threema/Session-style (X25519 + AEAD) | |
| # Uses ChaCha20-Poly1305 (built-in since OTP 24) as closest built-in equivalent to XSalsa20-Poly1305 | |
| # (exact XSalsa20 requires external lib or NIF – not possible with pure :crypto) | |
| defmodule SimpleP2P do | |
| @curve :x25519 | |
| @aead :chacha20_poly1305 | |
| @key_len 32 | |
| @nonce_len 12 | |
| @tag_len 16 | |
| @info "simple-p2p-encryption-2026" | |
| # Encrypt (ephemeral → receiver static public key) | |
| def encrypt(message, receiver_pub) when byte_size(receiver_pub) == 32 do | |
| {:ok, {ephemeral_pub, ephemeral_priv}} = :crypto.generate_key(@curve, nil) | |
| shared = :crypto.compute_key(@curve, receiver_pub, ephemeral_priv) | |
| key = KDF.hkdf(:sha256, shared, @info, @key_len) | |
| nonce = :crypto.strong_rand_bytes(@nonce_len) | |
| ciphertext = | |
| :crypto.crypto_one_time_aead(@aead, key, nonce, message, <<>>, @tag_len, true) | |
| |> then(fn {ct, tag} -> ct <> tag end) | |
| ephemeral_pub <> nonce <> ciphertext | |
| end | |
| # Decrypt (using receiver's long-term private key) | |
| def decrypt(envelope, receiver_priv) when byte_size(receiver_priv) == 32 do | |
| <<ephemeral_pub::binary-32, nonce::binary-12, ciphertext_tag::binary>> = envelope | |
| shared = :crypto.compute_key(@curve, ephemeral_pub, receiver_priv) | |
| key = KDF.hkdf(:sha256, shared, @info, @key_len) | |
| ciphertext_size = byte_size(ciphertext_tag) - @tag_len | |
| <<ciphertext::binary-size(ciphertext_size), tag::binary-size(@tag_len)>> = ciphertext_tag | |
| case :crypto.crypto_one_time_aead(@aead, key, nonce, ciphertext, <<>>, tag, false) do | |
| plaintext when is_binary(plaintext) -> {:ok, plaintext} | |
| :error -> {:error, :decryption_failed} | |
| end | |
| end | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment