Skip to content

Instantly share code, notes, and snippets.

@5HT
Created February 8, 2026 04:13
Show Gist options
  • Select an option

  • Save 5HT/cc464cf88af6ed1e14769fa43df32a5f to your computer and use it in GitHub Desktop.

Select an option

Save 5HT/cc464cf88af6ed1e14769fa43df32a5f to your computer and use it in GitHub Desktop.
v2.txt
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