Upgrade to Pro — share decks privately, control downloads, hide ads and more …

End-to-End Encryption Saves Lives. You Can Star...

Avatar for sylph01 sylph01
August 09, 2025

End-to-End Encryption Saves Lives. You Can Start Saving Lives With Ruby, Too (RubyConf Taiwan 2025 ver.)

Avatar for sylph01

sylph01

August 09, 2025
Tweet

More Decks by sylph01

Other Decks in Programming

Transcript

  1. End-to-End Encryption Saves Lives. You Can Start Saving Lives With

    Ruby, Too Ryo Kajiwara/ 梶原 龍 (sylph01) 2025/8/9 @ RubyConf Taiwan + COSCUP 2025 1
  2. I flew from 松山 (MYJ) to 松山 (TSA) again It

    says Ehime and Matsuyama but this is the SweetMe Hotspring Resort ( 水美温泉會館 ) in Xinbeitou 4
  3. I do stuff Play rhythm games (especially DanceDanceRevolution) Play the

    bassoon/contrabassoon and DJ gear Ride a lot of trains (Rails!) (travelled on 99% of JR) Build keyboards Organized RubyKaigi 2025 as a Local Organizer if anything catches your interest let's talk! 5
  4. And I do stuff that is more relevant to this

    talk: Freelance web developer focused on Digital Identity and Security Worked/ing on writing/editing and implementing standards HTTPS in Local Network CG / Web of Things WG @ W3C, OAuth / Messaging Layer Security WG @ IETF Worked as an Officer of Internet Society Japan Chapter (2020-23) 6
  5. The Usual Disclaimer Cryptographic API can be very easy to

    misuse Operational Security is also very difficult I've done my research, but I don't consider myself a cryptography expert a.k.a. "I am not djb" If you're not sure, please have your system audited by a security expert before going to production also I don't have a PhD/Master's degree in this field so yeah... 7
  6. Notes on Production Readiness The MLS implementation is still in

    progress Passes most test vectors Lacks validation on error cases Client implementation is underway Hopefully release 1.0 by the next IETF (2025/11)...? I was aiming for 1.0 before IETF 126 (2025/7) but yeah... 8
  7. 9

  8. I happen to be known as the SMTP をやめろ -guy

    SMTP をやめろ = lit. "stop using SMTP"; more like "SMTP is dead" 11
  9. SMTP mail lacks two things: 1) Strong identity 2) End-to-end

    Encryption MLS also helps with the identity part (with the full architecture), but that is service dependent 12
  10. End-to-End Encryption = The people who run the service cannot

    read your messages LINE, Facebook Messenger, Signal, WhatsApp, ... They each have their own version of E2EE Some apps' E2EE are opt-in 13
  11. E2EE is more important than ever Increasing tension in international

    affairs Powerful people wanting to control the Internet even in democratic countries! Provides a safe method to communicate under oppression / in war zones 15
  12. Hey, but doesn't E2EE help criminals? E2EE is being targeted

    by authorities even in democratic countries! Even if you ban E2EE, criminals will use it anyways Banning E2EE disproportionately harms vulnerable people 16
  13. Messaging Layer Security More interoperable version of doing E2EE MLS

    provides key exchange Protocol (RFC 9420), Architecture (RFC 9750) Today's topic is the implementation of the Protocol Now we (kinda) have RFC 9420 in Ruby! for MLS Architecture getting an RFC number 20
  14. mls gem already exists (for a defunct website...) It exists

    since 2012, so it predates the 00 Internet-Draft of Messaging Layer Security (2018/2) 22
  15. Melos https:/ /github.com/sylph01/melos Taken from a novel that is in

    most Japanese middle school/high school textbooks 23
  16. 24

  17. Basics: Symmetric vs Asymmetric Cryptography Symmetric crypto: one key used

    for both encryption and decryption Asymmetric crypto: Also known as Public Key Cryptography Alice keeps a pair of keys, public and private Bob can encrypt a plaintext using Alice's Public Key Then Alice decrypts the ciphertext with her Private Key 26
  18. Building blocks: HPKE "Encrypt with Public Key, Decrypt with Private

    Key" , formalized A combination of the following: Key Encapsulation Mechanism (KEM) ( ≒ asymmetric crypto) Key Derivation Function (KDF) ( ≒ hash) Authenticated Encryption with Associated Data (AEAD) (= symmetric crypto) Available in Ruby: hpke gem Seen this? I've talked about this at RubyConf Taiwan 2023 27
  19. Security Characteristics Forward Secrecy: messages sent at a certain point

    in time are secure in the face of later compromise of a group member secure against "harvest now, decrypt later" attack RFC 9420, Section 16.6 28
  20. Security Characteristics Post-Compromise Security: messages are secure even if a

    group member was compromised at some point in the past Each member updates their key so that group secrets are not always encrypted with private keys that have been compromised RFC 9420, Section 16.6 29
  21. 2 person is easy # both party starts with chain

    key 0 chain_key[0] = "some common secret" # when sending a message... message_key[n] = hmac_sha256(chain_key[n], 0x02) # encrypt the message #(n) using message_key[n] chain_key[n+1] = hmac_sha256(chain_key[n], 0x01) Symmetric Key Ratchet part of the Double Ratchet algorithm 31
  22. 3+ person is difficult Extend 2-person method? Number of edges

    in an n-node complete graph is O(n^2) 32
  23. Trees with arrays Complete balanced trees can be described with

    a flat array For example, the tree on the right will be written as: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, nil, 11, nil, nil, nil] 34
  24. Trees with arrays Leaf nodes are always even indexed Parent

    nodes are always odd indexed We define operations such as left , right , parent , sibling , ... based on the array-based tree's characteristics 35
  25. Trees with arrays The algorithms are described in Appendix C

    of RFC 9420 Also this comes with a serialization/deserialization format! 36
  26. Where's the Tree? Users in a group will be assigned

    a leaf in the ratchet_tree and the secret_tree with the same index. 38
  27. Basically You want to agree with other users in the

    group upon a common secret: epoch_secret[n], where n is the epoch number 39
  28. Secret Tree If you have a common secret, the group

    can derive the same (symmetric) keys for encrypting messages 40
  29. Secret Tree def self.populate_tree_impl(suite, tree, index, secret) tree.array[index] = {

    ... } unless Melos::Tree.leaf?(index) left_secret = Melos::Crypto.expand_with_label(suite, secret, "tree", "left", suite.kdf.n_h) right_secret = Melos::Crypto.expand_with_label(suite, secret, "tree", "right", suite.kdf.n_h) populate_tree_impl(suite, tree, Melos::Tree.left(index), left_secret) populate_tree_impl(suite, tree, Melos::Tree.right(index), right_secret) end end From the base encryption secret, we recursively populate the tree down to its leaves file lib/melos/secret_tree.rb 41
  30. Secret Tree The actual ratcheting part: def self.ratchet_application(suite, tree, leaf_index)

    node_index = leaf_index * 2 generation = tree.array[node_index]['next_application_ratchet_secret_generation'] application_ratchet_secret = tree.array[node_index]['application_ratchet_secret'] application_nonce = Melos::Crypto.derive_tree_secret(suite, application_ratchet_secret, "nonce", generation, suite.hpke.n_n) application_key = Melos::Crypto.derive_tree_secret(suite, application_ratchet_secret, "key", generation, suite.hpke.n_k) next_application_ratchet_secret = Melos::Crypto.derive_tree_secret(suite, application_ratchet_secret, "secret", generation, suite.kdf.n_h) tree.array[node_index]['next_application_ratchet_secret_generation'] = generation + 1 tree.array[node_index]['application_ratchet_secret'] = next_application_ratchet_secret tree.array[node_index]['application_nonce'] = application_nonce tree.array[node_index]['application_key'] = application_key end file lib/melos/secret_tree.rb 42
  31. Secret Tree It essentially boils down to this: key =

    Crypto.derive_tree_secret(ratchet_secret[n], "key", n) nonce = Crypto.derive_tree_secret(ratchet_secret[n], "nonce", n) ratchet_secret[n + 1] = Crypto.derive_tree_secret(ratchet_secret[n], "secret", n) This gives you the key and nonce to encrypt the actual messages. Compare with this (two-party hash ratcheting): chain_key[0] = "some common secret" message_key[n] = hmac_sha256(chain_key[n], 0x02) chain_key[n+1] = hmac_sha256(chain_key[n], 0x01) 43
  32. Key Schedule With the last epoch_secret and a common commit_secret

    , the group can agree upon the next epoch_secret see lib/melos/key_schedule.rb and test/test_key_schedule.rb 44
  33. TreeKEM User generates a random path_secret at their leaf The

    parent's path_secret is calculated using the child's path_secret Calculate that up to the root The commit_secret is calculated from the root's path_secret 48
  34. TreeKEM When 0 creates its UpdatePath (yellow) We find the

    copath nodes along the UpdatePath (green) If all ParentNode s in the tree are populated, we encrypt the path secret to the key of its copath node Public keys for parent nodes are derived from path_secret s 50
  35. TreeKEM We can see that's possible in a 2-leaf tree

    0 creates an UpdatePath, node 1 has a path_secret the path secret is encrypted to 2 's public key node 2 knows the private key, so 2 can decrypt 51
  36. TreeKEM What happens if there are blanks in the tree?

    We calculate the resolution of the copath node to figure out which node's keys are available. Then, we encrypt the path secret of the UpdatePath node to each key 52
  37. TreeKEM The resolution of the node indicated left is [3,

    2] The indicated node has unmerged leaves so its resolution is itself + list of unmerged leaves 53
  38. TreeKEM The resolution of the node indicated left is [3,

    2, 9, 14] The resolution is used to figure out the set of keys that are necessary to encrypt to every node under that node 54
  39. TreeKEM So actually what you do is: Calculate path_secret s

    for each node on UpdatePath then encrypt the path_secret s to each key on the resolution of the copath node 55
  40. TreeKEM Encrypting the commit_secret itself and sending it to everyone

    works but it's O(n) (n = # of members) Encrypting the commit_secret along the copath reduces the number of encryptions needed down to O(log n) 56
  41. Summary Secret Tree gives you the per- message sender keys

    based on epoch_secret Key Schedule gives you the next epoch_secret based on current epoch_secret and commit_secret TreeKEM gives you the commit_secret 57
  42. Sending Application Messages Application messages (actual messages) are sent in

    PrivateMessage s framed_content = Melos::Struct::FramedContent.create(...) authenticated_content = Melos::Struct::AuthenticatedContent.create(...) authenticated_content.sign(...) app_message = Melos::Struct::PrivateMessage.protect( authenticated_content, secret_tree, ... ) 58
  43. Evolution of a Group A group is updated through messages

    called proposals and commits Proposals Add and remove users Notifies an update of user's leaf key Injects Pre-Shared Keys Commits will fix those information and advances the group's epoch UpdatePath s are conveyed in the Commit 59
  44. Evolution of a Group A Group is created by adding

    a user's LeafNode into a 0-node group Starts with a random epoch_secret def init_group(node) # single node, a leaf node containing an HPKE PK and credential for the creator @ratchet_tree = [node] @leaf_index = 0 @tree_hash = Melos::Struct::RatchetTree.root_tree_hash(@cipher_suite, @ratchet_tree) @confirmed_transcript_hash = '' @epoch_secret = SecureRandom.random_bytes(@cipher_suite.kdf.n_h) ... @group_initialized = true end lib/group.rb from a work-in-progress version 60
  45. Evolution of a Group Users who want to join the

    group publishes their identity (including its public key) to a Directory using a KeyPackage They will receive a Welcome message that has the group state inside it Figure 2 in RFC 9420 Section 3.2. The Delivery Service is out of scope for this implementation 61
  46. Evolution of a Group User A creates an initial group

    To add User B , A sends an Add proposal adding B , then Commit s A sends a Welcome message to introduce B into the group Same for adding C Figure 3 in RFC 9420 Section 3.2. 62
  47. Evolution of a Group When Add ing: Add the leaf_node

    from the KeyPackage in the proposal to the leftmost empty node Then add the leaf index to intermediate nodes' unmerged list 63
  48. Evolution of a Group User B sends an Update proposal

    notifying the group of its leaf key update Then User A Commit s that update to include it in the epoch advancement Figure 4 in RFC 9420 Section 3.2. 64
  49. Evolution of a Group When Update ing: Replace the leaf

    node of the sender with the leaf_node inside the Update proposal Then blank all nodes on its direct path up to the root Updating the keys regularly gives the Group Post-Compromise Security 65
  50. Evolution of a Group When removing a user: Z sends

    a Remove proposal and a Commit B cannot decrypt the path_secret included in the Commit 's UpdatePath , so B will not know the next epoch_secret 66
  51. Evolution of a Group When Remove ing: Blank the specified

    node in the proposal Then blank all nodes on its direct path up to the root If the right half of the tree is fully empty, the tree is shrunk 3 becomes the new root 67
  52. Evolution of a Group When are intermediate parent nodes filled?

    During the processing of a Commit When processing a Commit , the UpdatePath inside the Commit is merged into the ratchet tree ParentNode s are created based on the UpdatePath content 68
  53. Stuff we are omitting here Injection of Pre-Shared Keys Transcript

    Hashes Hashes that summarize the proposals/commits taken place in the last epoch Signing/Verification of messages There are public messages and private messages 69
  54. 70

  55. Current implementation state Passes most test vectors Updates from RubyKaigi

    Parsing messages now use a StringIO instead of two String s Moved group state into a Group object Needs a user-friendly client API Needs interoperability testing 72
  56. Work-in-progress Client Interface cipher_suite_id = 1 client = Melos::Client.new(cipher_suite_id) #

    generate key pairs key_package = Melos::KeyPackage.new(client) # create KP to publish to DS leaf_node = key_package.create_leaf_node('credential_name') node = Melos::Struct::Node.new_leaf_node(leaf_node) group = Melos::Group.new(client, 'group_id') group.init_group(node) # create a 1-node group 73
  57. Work-in-progress Client Interface client2 = Melos::Client.new(cipher_suite_id) key_package2 = Melos::KeyPackage.new(client2) leaf_node2

    = key_package2.create_leaf_node('credential_name2') key_package_message2 = key_package2.create_message(leaf_node2) # User 1 creates add proposal prop = group.create_add_proposal( key_package_message2, client.signature_private_key) # something like this...? commit = create_commit(prop, ...) group.apply_commit(commit) group2.apply_welcome(welcome) 74
  58. :contribute-chance: "Refactoring" Wrapping raw values into classes Group is being

    worked on RatchetTree is now represented as a raw array Interoperability testing Learn other implementations and create test scenarios There is an automated interop suite that uses gRPC :contribute-chance: is a Slack emoji in #ruby-jp used for indicating good first issues 75
  59. HPKE using OpenSSL's API HPKE is nice, and OpenSSL itself

    has APIs that do this Talked about it at RubyConf Taiwan 2023 Haven't worked on it much since 77
  60. HPKE using OpenSSL's API But is it a good idea?

    Protocols need the whole cipher suite Not only encap/decap and open/seal Also want access to constants such as hash/key length OpenSSL's HPKE context remembers which algorithm to use, but to use them separately you have to call them separately 78
  61. Example def self.sender_data_key(suite, sender_data_secret, ciphertext) ciphertext_sample = ciphertext[0..(suite.kdf.n_h - 1)]

    expand_with_label(suite, sender_data_secret, "key", ciphertext_sample, suite.hpke.n_k) end def self.sender_data_nonce(suite, sender_data_secret, ciphertext) ciphertext_sample = ciphertext[0..(suite.kdf.n_h - 1)] expand_with_label(suite, sender_data_secret, "nonce", ciphertext_sample, suite.hpke.n_n) end n_h is hash length, n_k and n_n is the key and nonce length for the symmetric crypto 79
  62. EC vs X25519/X448 APIs in OpenSSL Checking correspondence of key

    pairs def self.signature_key_pair_corresponds?(suite, private_key, public_key) private_pkey = suite.pkey.deserialize_private_signature_key(private_key) public_pkey = suite.pkey.deserialize_public_signature_key(public_key) if suite.pkey.equal?(Melos::Crypto::CipherSuite::X25519) || suite.pkey.equal?(Melos::Crypto::CipherSuite::X448) # is an Edwards curve; check equality of the raw public key private_pkey.raw_public_key == public_pkey.raw_public_key else # is an EC; check equality of the public key Point private_pkey.public_key == public_pkey.public_key end end 80
  63. EC vs X25519/X448 APIs in OpenSSL Getting EC public/private keys

    in UncompressedPointRepresentation form def self.derive_key_pair(suite, secret) pkey = suite.hpke.kem.derive_key_pair(secret) if suite.pkey.equal?(Melos::Crypto::CipherSuite::X25519) || suite.pkey.equal?(Melos::Crypto::CipherSuite::X448) # is an Edwards curve [pkey.raw_private_key, pkey.raw_public_key] else # is an EC [pkey.private_key.to_s(2), pkey.public_key.to_bn.to_s(2)] end end 81
  64. Additional features in OpenSSL OpenSSL 3.5.0 ships with post-quantum cryptography

    ML-KEM, ML-DSA, SLH-DSA There is an Internet-Draft that adds ML-KEM in MLS 82
  65. Grand Unifying Cryptography API Different platforms depend on different cryptographic

    libraries Desktop: OpenSSL Compatible (to an extent) with LibreSSL, BoringSSL, and the like Browser: Web Crypto API Embedded: Mbed TLS, wolfSSL, ... We have Mbed TLS support in PicoRuby Can we have a unified API wrapper for these libraries? Actually ruby-wasm has OpenSSL compiled in wasm, but doing it through browser APIs is faster 85
  66. Grand Unifying Cryptography API Very quick example of calling Web

    Crypto API's random number generator require "js" array = JS::eval('return new Uint8Array(16)') JS.global[:window][:crypto].getRandomValues(array) p array 86
  67. 87

  68. Why do you need End-to-End Encryption in Ruby? Because... This

    is a DanceDanceRevolution reference. see: MAX.(period), Over the "Period" 89
  69. Ruby needs them to stay relevant or else people would

    just use Python, Go, Rust, whatever the cool kids use these days Actually, Python doesn't have an MLS implementation yet, so it's a win for Ruby 90
  70. Takeaways We have interoperable ways of doing E2EE We (kinda)

    have this in Ruby too Please use secure messaging in your daily life 92
  71. Shoutouts The Messaging Layer Security Working Group @ IETF Protocol

    implementers in Ruby (the list is growing!) RubyConf Taiwan 2025 Organizers 93
  72. Questions? / Comments? Twitter: @s01 or Fediverse: @[email protected] also find

    me in the venue / at drinkups! I will be at the Official Code Party 94
  73. 95

  74. It's not just a pack and unpack MLS has variable

    length vectors and optional values in their structs struct { opaque group_id<V>; uint64 epoch; ContentType content_type; opaque authenticated_data<V>; opaque encrypted_sender_data<V>; opaque ciphertext<V>; } PrivateMessage; 97
  75. Variable length vectors struct { uint32 fixed<0..255>; opaque variable<V>; }

    StructWithVectors; Based on variable-length integer encoding in RFC 9000, Section 16 2-bit prefix if 00 , length is encoded with 6 bits if 01 , length is encoded with 14 bits if 11 , length is encoded with 30 bits 98
  76. Optional values struct { uint8 present; select (present) { case

    0: struct{}; case 1: T value; }; } optional<T>; 99
  77. Define structs like this class Melos::Struct::PrivateMessage < Melos::Struct::Base attr_reader :group_id,

    :epoch, :content_type, :authenticated_data, :encrypted_sender_data, :ciphertext STRUCT = [ [:group_id, :vec], [:epoch, :uint64], [:content_type, :uint8], [:authenticated_data, :vec], [:encrypted_sender_data, :vec], [:ciphertext, :vec] ] 100
  78. Melos::Struct::Base #initialize , .new_and_rest : deserializer #raw : serializer These

    operate based on the STRUCT constant of the class lib/melos/struct/base.rb 101
  79. Deserialize each element like this def deserialize_elem(buf, type, type_param) case

    type when :uint8 value = buf.byteslice(0, 1).unpack1('C') buf = buf.byteslice(1..) when :uint16 value = buf.byteslice(0, 2).unpack1('S>') buf = buf.byteslice(2..) (snip) when :vec value, buf = Melos::Vec.parse_vec(buf) (snip) lib/melos/struct/base.rb; I'm likely rewriting this with a StringIO instead of using strings as buffers 102
  80. Sometimes a class is nested class Melos::Struct::FramedContent < Melos::Struct::Base (snip)

    STRUCT = [ [:group_id, :vec], [:epoch, :uint64], [:sender, :class, Melos::Struct::Sender], [:authenticated_data, :vec], [:content_type, :uint8], (snip) ] lib/melos/struct/structs.rb 103
  81. So we recursively call new when :class value, buf =

    type_param.send(:new_and_rest, buf) when :classes # prefix, length = buf.get_prefix_and_length # puts "#{prefix}, #{length}" vec, buf = Melos::Vec.parse_vec(buf) value = [] while (vec.bytesize > 0) current_instance, vec = type_param.send(:new_and_rest, vec) value << current_instance end lib/melos/struct/structs.rb 104
  82. We sometimes have values that depend on other values class

    Melos::Struct::Sender < Melos::Struct::Base attr_reader :sender_type, :leaf_index, :sender_index STRUCT = [ [:sender_type, :uint8], [:leaf_index, :select, ->(ctx){ctx[:sender_type] == 0x01}, :uint32], [:sender_index, :select, ->(ctx){ctx[:sender_type] == 0x02}, :uint32], ] lib/melos/struct/structs.rb 105
  83. We call the Proc with the current context to handle

    that def deserialize_select_elem_with_context( buf, context, predicate, type, type_param) if predicate.(context) deserialize_elem(buf, type, type_param) else [nil, buf] end end lib/melos/struct/base.rb 106