├── README.md ├── bin ├── cable-id ├── cable-info ├── cable-ping ├── cable-send ├── gen-cable-username ├── gen-i2p-hostname └── gen-tor-hostname ├── cable ├── cabled ├── cms ├── comm ├── crypto ├── fetch ├── loop ├── send └── validate ├── conf ├── cabled ├── extensions.cnf ├── profile └── rfc3526-modp-18.pem ├── doc └── cable.txt ├── makefile ├── pkg └── cables-x.y.ebuild ├── share └── cable-info.desktop ├── src ├── daemon.c ├── daemon.h ├── hex2base32.c ├── mhdrop.c ├── process.c ├── process.h ├── server.c ├── server.h ├── service.c ├── service.h ├── su │ └── dee │ │ └── i2p │ │ └── EepPriv.java ├── util.c └── util.h └── test ├── curl ├── logger ├── oakley-group-2.pem └── simulate /README.md: -------------------------------------------------------------------------------- 1 | # Secure Cables Communication 2 | 3 | Secure and anonymous communication using email-like addresses, pioneered in [Liberté Linux](http://dee.su/liberte). 4 | Cables communication is Liberté's pivotal component for enabling anyone to communicate safely and covertly in [hostile environments](http://dee.su/liberte-motivation). 5 | 6 | See the [project site](http://dee.su/cables) for further details, and read the [Wiki](https://github.com/mkdesu/cables/wiki) for implementation and [deployment](https://github.com/mkdesu/cables/wiki/deployment) details. 7 | 8 | License: [GPLv2](http://www.gnu.org/licenses/gpl-2.0.html). 9 | -------------------------------------------------------------------------------- /bin/cable-id: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # Setup environment with needed environment vars 4 | . /etc/cable/profile 5 | 6 | 7 | error() { 8 | echo "cable-id: $@" 1>&2 9 | exit 1 10 | } 11 | 12 | torhost=${CABLE_TOR}/hidden_service/hostname 13 | i2phost=${CABLE_I2P}/eepsite/hostname 14 | username=${CABLE_CERTS}/certs/username 15 | 16 | undefined="undefined" 17 | 18 | 19 | case "$1" in 20 | user) 21 | if [ ! -e ${username} ]; then 22 | echo "${undefined}" 23 | else 24 | username=`cat ${username} | tr -cd a-z2-7` 25 | [ ${#username} = 32 ] || error "bad username" 26 | echo "${username}" 27 | fi 28 | ;; 29 | 30 | tor) 31 | if [ ! -e ${torhost} ]; then 32 | echo "${undefined}" 33 | else 34 | torhost=`cat ${torhost} | tr -cd '[:alnum:].-' | tr '[:upper:]' '[:lower:]'` 35 | [ ${#torhost} != 0 ] || error "bad Tor hostname" 36 | echo "${torhost}" 37 | fi 38 | ;; 39 | 40 | i2p) 41 | if [ ! -e ${i2phost} ]; then 42 | echo "${undefined}" 43 | else 44 | i2phost=`cat ${i2phost} | tr -cd '[:alnum:].-' | tr '[:upper:]' '[:lower:]'` 45 | [ ${#i2phost} != 0 ] || error "bad I2P hostname" 46 | echo "${i2phost}" 47 | fi 48 | ;; 49 | 50 | *) 51 | error "param: user|tor|i2p" 52 | ;; 53 | esac 54 | -------------------------------------------------------------------------------- /bin/cable-info: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | cableid=cable-id 4 | undefined="undefined" 5 | title="Cables Communication Identity" 6 | 7 | username=`${cableid} user` 8 | torhost=`${cableid} tor | sed 's/\.onion$//'` 9 | i2phost=`${cableid} i2p | sed 's/\.b32\.i2p$//'` 10 | 11 | 12 | if [ "${username}" = "${undefined}" -o "${torhost}${i2phost}" = "${undefined}${undefined}" ]; then 13 | message="${title} 14 | 15 | Cables communication addresses have not been configured. 16 | 17 | When using cables in Liberté Linux, this is typically a result of disabled persistence: booting from an ISO image in a virtual machine, booting from an actual CD, or write-protecting the boot media. 18 | 19 | In order to enable persistence, install Liberté Linux to a writable media, such as a USB stick or an SD card." 20 | 21 | exec zenity --error --title="${title}" --text="${message}" 22 | fi 23 | 24 | 25 | splitre='s@\([[:alnum:]]\{4\}\)\([[:alnum:]]\{4\}\)\?@\1\2@g' 26 | 27 | username=`echo "${username}" | sed "${splitre}"` 28 | addrs= 29 | 30 | if [ "${torhost}" != "${undefined}" ]; then 31 | torhost=`echo "${torhost}" | sed "${splitre}"`.onion 32 | addrs="${addrs} ${username}@${torhost}" 33 | fi 34 | if [ "${i2phost}" != "${undefined}" ]; then 35 | i2phost=`echo "${i2phost}" | sed "${splitre}"`.b32.i2p 36 | addrs="${addrs} ${username}@${i2phost}" 37 | fi 38 | 39 | 40 | message="${title} 41 | 42 | You can use the following addresses for cables communication via Claws-Mail: 43 | ${addrs} 44 | 45 | Always check the username of incoming messages — its authenticity is guaranteed by the cables communication protocol. When manually reading addresses, keep in mind that only digits 27 are used, the rest are letters. 46 | 47 | You can set either address in Claws-Mail account settings. Upon startup, Claws-Mail will reset the account to Tor-based address if the configured address is not one of the above." 48 | 49 | exec zenity --info --title="${title}" --text="${message}" 50 | -------------------------------------------------------------------------------- /bin/cable-ping: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # Setup environment with needed environment vars 4 | . /etc/cable/profile 5 | 6 | 7 | # Command-line parameters 8 | if [ $# != 1 ]; then 9 | echo "Format: $0 user@host" 10 | exit 1 11 | fi 12 | 13 | 14 | error() { 15 | echo "cable-ping: $@" 1>&2 16 | exit 1 17 | } 18 | 19 | 20 | emailregex="${CABLE_REGEX}" 21 | cableregex="LIBERTE CABLE [[:alnum:]._-]+" 22 | maxresp=128 23 | addr="$1" 24 | 25 | if ! echo x "${addr}" | egrep -q "^x ${emailregex}$"; then 26 | error "unsupported address" 27 | fi 28 | 29 | 30 | user=`echo "${addr}" | cut -d@ -f1` 31 | host=`echo "${addr}" | cut -d@ -f2` 32 | url=http://"${host}"/"${user}"/request/ver 33 | 34 | 35 | # Pipe eats curl's error status, if any 36 | resp=`curl -sSfg "${url}" 2>&1 | head -c ${maxresp} | tr -cd '[:alnum:][:blank:]:()/._-'` 37 | 38 | if echo x "${resp}" | grep -q "^x curl:"; then 39 | error "communication error: ${resp}" 40 | elif echo x "${resp}" | egrep -q "^x ${cableregex}$"; then 41 | echo "${resp}" 42 | else 43 | error "unexpected output: ${resp}" 44 | fi 45 | -------------------------------------------------------------------------------- /bin/cable-send: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # /etc/sudoers should contain something like 4 | # anon ALL = (cable) NOPASSWD: /usr/libexec/cable/send "" 5 | 6 | . /etc/cable/profile 7 | exec sudo -n -u cable ${CABLE_HOME}/send "$@" 8 | -------------------------------------------------------------------------------- /bin/gen-cable-username: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # This command can be run via "sudo -u anon" 4 | . /etc/cable/profile 5 | 6 | 7 | # NOTE: PSS signatures support in certificates 8 | # (but not in CMS) is available in OpenSSL 1.0.1 9 | 10 | sslconf=${CABLE_CONF}/extensions.cnf 11 | base32=${CABLE_HOME}/hex2base32 12 | 13 | certdir=${CABLE_CERTS}/certs 14 | keysdir=${CABLE_CERTS}/private 15 | 16 | v1certdir=${CABLE_CERTS}/../ssl/certs 17 | v1keysdir=${CABLE_CERTS}/../ssl/private 18 | 19 | certdirtmp=${certdir}.tmp 20 | keysdirtmp=${keysdir}.tmp 21 | 22 | # reqsubj not affected by locale 23 | rsabits=8192 24 | shabits=512 25 | reqsubj='/O=LIBERTE CABLE/CN=Anonymous' 26 | crtdays=18300 27 | 28 | 29 | # Fail if CA dir already exists 30 | if [ -e ${certdir} -a -e ${keysdir} ]; then 31 | echo ${certdir} and ${keysdir} already exist 32 | exit 1 33 | fi 34 | 35 | 36 | # Create temporary directories (erase previous ones, if exist) 37 | rm -rf ${certdirtmp} ${keysdirtmp} ${certdir} ${keysdir} 38 | mkdir ${certdirtmp} ${keysdirtmp} 39 | 40 | 41 | # Migrate v1 directories, if present 42 | if [ ! -e ${v1certdir}/username ]; then 43 | 44 | # Generate RSA key + X.509 self-signed root CA certificate 45 | openssl req -batch -new -utf8 -subj "${reqsubj}" \ 46 | -newkey rsa:${rsabits} -nodes -keyout ${keysdirtmp}/root.pem \ 47 | -x509 -days ${crtdays} -sha${shabits} -out ${certdirtmp}/ca.pem \ 48 | -config "${sslconf}" -extensions root 49 | 50 | 51 | # Save 32-character Base32 username (root CA SHA-1 hash) 52 | fingerprint=`openssl x509 -in ${certdirtmp}/ca.pem -outform der | sha1sum | head -c 40` 53 | [ ${#fingerprint} = 40 ] 54 | ${base32} ${fingerprint} > ${certdirtmp}/username 55 | 56 | 57 | # Use the same key for ephemeral DH peer keys authentication 58 | ln -s root.pem ${keysdirtmp}/sign.pem 59 | 60 | 61 | # Generate X.509 verification certificate 62 | openssl req -batch -new -utf8 -subj "${reqsubj}" \ 63 | -key ${keysdirtmp}/sign.pem | \ 64 | openssl x509 -req -days ${crtdays} -sha${shabits} -out ${certdirtmp}/verify.pem \ 65 | -CA ${certdirtmp}/ca.pem -CAkey ${keysdirtmp}/root.pem \ 66 | -CAcreateserial -CAserial ${certdirtmp}/certs.srl \ 67 | -extfile "${sslconf}" -extensions verify 2>/dev/null 68 | 69 | else 70 | echo "Migrating v1.0 certificates" 71 | 72 | # NTFS-3G denies 'getfattr -h' on symlinks for non-root user 73 | rsync -aHA ${v1certdir}/ ${certdirtmp} 74 | rsync -aHA ${v1keysdir}/ ${keysdirtmp} 75 | 76 | rm ${certdirtmp}/encrypt.pem ${keysdirtmp}/decrypt.pem 77 | fi 78 | 79 | 80 | # Sanity checks 81 | checks=` 82 | openssl verify -x509_strict -check_ss_sig -policy_check -purpose crlsign \ 83 | -CAfile ${certdirtmp}/ca.pem -CApath /dev/null ${certdirtmp}/ca.pem 84 | openssl verify -x509_strict -check_ss_sig -policy_check -purpose smimesign \ 85 | -CAfile ${certdirtmp}/ca.pem -CApath /dev/null ${certdirtmp}/verify.pem 86 | ` 87 | 88 | test "${checks}" = "${certdirtmp}/ca.pem: OK 89 | ${certdirtmp}/verify.pem: OK" 90 | 91 | 92 | # Commit new directories 93 | chmod 710 ${certdirtmp} ${keysdirtmp} 94 | chmod 640 ${certdirtmp}/* ${keysdirtmp}/* 95 | 96 | mv -T ${keysdirtmp} ${keysdir} 97 | mv -T ${certdirtmp} ${certdir} 98 | -------------------------------------------------------------------------------- /bin/gen-i2p-hostname: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # This command can be run via "sudo -u anon" 4 | . /etc/cable/profile 5 | 6 | 7 | i2pdir=${CABLE_I2P}/eepsite 8 | eeppriv=${CABLE_HOME}/eeppriv.jar 9 | 10 | i2pdirtmp=${i2pdir}.tmp 11 | 12 | 13 | # Fail if dir already exists 14 | if [ -e ${i2pdir} ]; then 15 | echo ${i2pdir} already exists 16 | exit 1 17 | fi 18 | 19 | 20 | # Create temporary directory (erase previous one, if exists) 21 | rm -rf ${i2pdirtmp} 22 | mkdir ${i2pdirtmp} 23 | 24 | 25 | # Generate eepPriv.dat and b32/b64 hostnames the same way that I2P does 26 | # JamVM's SecureRandom uses /dev/urandom 27 | java -jar ${eeppriv} ${i2pdirtmp} 28 | 29 | 30 | # Sanity checks 31 | if [ ! -s ${i2pdirtmp}/eepPriv.dat -o ! -s ${i2pdirtmp}/hostname -o ! -s ${i2pdirtmp}/hostname.b64 ]; then 32 | echo Failed to create eepsite key 33 | exit 1 34 | fi 35 | 36 | 37 | # Commit new directory 38 | mv -T ${i2pdirtmp} ${i2pdir} 39 | -------------------------------------------------------------------------------- /bin/gen-tor-hostname: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # This command can be run via "sudo -u anon" 4 | . /etc/cable/profile 5 | 6 | 7 | # https://gitweb.torproject.org/tor.git/blob/HEAD:/doc/spec/tor-spec.txt 8 | # https://trac.torproject.org/projects/tor/wiki/TheOnionRouter/HiddenServiceNames 9 | 10 | tordir=${CABLE_TOR}/hidden_service 11 | base32=${CABLE_HOME}/hex2base32 12 | 13 | tordirtmp=${tordir}.tmp 14 | 15 | 16 | # Fail if dir already exists 17 | if [ -e ${tordir} ]; then 18 | echo ${tordir} already exists 19 | exit 1 20 | fi 21 | 22 | 23 | # Create temporary directory (erase previous one, if exists) 24 | rm -rf ${tordirtmp} 25 | mkdir ${tordirtmp} 26 | 27 | 28 | # Generate RSA-1024 with exponent 65537, as per spec 29 | # Use genrsa instead of genpkey, for result similarity 30 | openssl genrsa -f4 -out ${tordirtmp}/private_key 1024 2>/dev/null 31 | 32 | 33 | # Tor hashes ASN.1 RSAPublicKey instead of SubjectPublicKeyInfo, 34 | # which contains RSAPublicKey at offset 22; it then converts the 35 | # first half of SHA-1 hash to Base32 36 | hex=`openssl pkey -in ${tordirtmp}/private_key -pubout -outform der 2>/dev/null \ 37 | | tail -c +23 | sha1sum | head -c 20` 38 | [ ${#hex} = 20 ] 39 | b32=`${base32} ${hex}` 40 | 41 | echo ${b32}.onion > ${tordirtmp}/hostname 42 | 43 | 44 | # Commit new directory 45 | mv -T ${tordirtmp} ${tordir} 46 | -------------------------------------------------------------------------------- /cable/cabled: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # Setup restricted environment 4 | # (rely on init via sudo to set up TMPDIR) 5 | . /etc/cable/profile 6 | 7 | 8 | # Variables 9 | daemon=${CABLE_HOME}/daemon 10 | queue=${CABLE_QUEUES}/queue 11 | rqueue=${CABLE_QUEUES}/rqueue 12 | 13 | mktempre='tmp\.[A-Za-z0-9]{10}' 14 | newmsgidre='[0-9a-f]{40}\.new' 15 | 16 | 17 | # Remove stale temporary directories with old timestamps 18 | find ${queue} -mindepth 1 -maxdepth 1 -regextype posix-egrep \ 19 | -regex "${queue}/${mktempre}" -mtime +1 -exec rm -rf {} \; 20 | find ${rqueue} -mindepth 1 -maxdepth 1 -regextype posix-egrep \ 21 | -regex "${rqueue}/${newmsgidre}" -mtime +1 -exec rm -rf {} \; 22 | 23 | 24 | # Let fuse-vfs inotify emulation stabilize 25 | sleep 30 26 | 27 | 28 | # Launch the inotify-based daemon 29 | exec "${daemon}" 30 | -------------------------------------------------------------------------------- /cable/cms: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # Encryption, decryption and verification of messages 4 | # NOTE: the "-e" switch above is important (script failure on error)! 5 | 6 | # WARNING: keys are provided as parameters to OpenSSL. 7 | # This script should be rewritten as a C program. 8 | 9 | # Variables 10 | cmd="$1" 11 | ssldir="$2" 12 | msgdir="$3" 13 | 14 | modp18=${CABLE_CONF}/rfc3526-modp-18.pem 15 | base32=${CABLE_HOME}/hex2base32 16 | 17 | sigalg=sha512 18 | encalg=aes256 19 | enchash=sha256 20 | 21 | 22 | # Command-line parameters 23 | if [ \( "${cmd}" != send -a "${cmd}" != peer -a "${cmd}" != recv \) \ 24 | -o ! -d "${ssldir}" -o ! -d "${msgdir}" ]; then 25 | echo "Format: $0 send|peer|recv " 26 | exit 1 27 | fi 28 | 29 | 30 | trap '[ $? = 0 ] || error failed' 0 31 | error() { 32 | logger -t cms -p mail.err "$@ (${msgdir##*/})" 33 | trap - 0 34 | exit 1 35 | } 36 | 37 | 38 | # Verifying the ca/verify/encrypt certificates triple 39 | # * parses and extracts the first certificate from each file 40 | # * verifies the certificates chain 41 | # * generates username from ca.pem and checks it against the given one 42 | verify_certs() { 43 | local cvedir="$1" name count fingerprint username 44 | 45 | for name in ca verify; do 46 | openssl x509 -in "${cvedir}"/${name}.pem \ 47 | -out "${cvedir}"/${name}.norm.pem 48 | mv -- "${cvedir}"/${name}.norm.pem "${cvedir}"/${name}.pem 49 | 50 | # Sanity check - OpenSSL should output exactly 1 certificate 51 | count=`grep -c -- '^-----BEGIN ' "${cvedir}"/${name}.pem` 52 | [ "${count}" = 1 ] 53 | done 54 | 55 | # certificates chain verification is also implicitly done later, 56 | # so not strictly necessary 57 | openssl verify -x509_strict -policy_check -purpose crlsign -check_ss_sig \ 58 | -CApath /dev/null -CAfile "${cvedir}"/ca.pem "${cvedir}"/ca.pem \ 59 | > "${cvedir}"/certs.vfy 2>&1 60 | openssl verify -x509_strict -policy_check -purpose smimesign \ 61 | -CApath /dev/null -CAfile "${cvedir}"/ca.pem "${cvedir}"/verify.pem \ 62 | >> "${cvedir}"/certs.vfy 2>&1 63 | 64 | if ! cmp -s -- "${cvedir}"/certs.vfy - < 100 | # 101 | # out: derive.pem, rpeer.sig[atomic] 102 | # 103 | # <--- rpeer.sig 104 | # 105 | # 106 | # in: message, username, {ca,verify}.pem, rpeer.sig 107 | # out: speer.sig[atomic], message.enc[atomic], {send,recv,ack}.mac 108 | # 109 | # ---> speer.sig, message.enc, send.mac 110 | # 111 | # 112 | # in: message.enc, username, send.mac, {ca,verify,derive}.pem, speer.sig 113 | # out: message, {recv,ack}.mac 114 | # 115 | # <--- recv.mac 116 | # 117 | # 118 | # 119 | # ---> ack.mac 120 | case ${cmd} in 121 | peer) 122 | rm -f -- "${msgdir}"/derive.pem "${msgdir}"/rpeer.der "${msgdir}"/rpeer.sig \ 123 | "${msgdir}"/rpeer.sig.tmp 124 | 125 | # generate ephemeral peer key 126 | openssl genpkey -paramfile "${modp18}" \ 127 | -out "${msgdir}"/derive.pem 128 | 129 | # extract and sign ephemeral public peer key 130 | openssl pkey -pubout -outform der \ 131 | -in "${msgdir}"/derive.pem \ 132 | -out "${msgdir}"/rpeer.der 133 | 134 | openssl cms -sign -noattr -binary -md ${sigalg} -nodetach -nocerts -outform pem \ 135 | -signer "${certdir}"/verify.pem \ 136 | -inkey "${keysdir}"/sign.pem \ 137 | -in "${msgdir}"/rpeer.der \ 138 | -out "${msgdir}"/rpeer.sig.tmp 139 | mv -- "${msgdir}"/rpeer.sig.tmp "${msgdir}"/rpeer.sig 140 | 141 | rm -- "${msgdir}"/rpeer.der 142 | ;; 143 | 144 | 145 | send) 146 | rm -f -- "${msgdir}"/derive.pem "${msgdir}"/speer.der "${msgdir}"/rpeer.der \ 147 | "${msgdir}"/speer.sig "${msgdir}"/shared.key "${msgdir}"/message.enc \ 148 | "${msgdir}"/send.mac "${msgdir}"/recv.mac "${msgdir}"/ack.mac \ 149 | "${msgdir}"/speer.sig.tmp "${msgdir}"/message.enc.tmp 150 | 151 | # verify certificates chain 152 | verify_certs "${msgdir}" 153 | 154 | # verify and extract signed recipient's ephemeral public peer key 155 | openssl cms -verify \ 156 | -x509_strict -policy_check -purpose smimesign -check_ss_sig -inform pem \ 157 | -CAfile "${msgdir}"/ca.pem -CApath /dev/null \ 158 | -certfile "${msgdir}"/verify.pem \ 159 | -in "${msgdir}"/rpeer.sig \ 160 | -out "${msgdir}"/rpeer.der 161 | 162 | # generate ephemeral peer key 163 | openssl genpkey -paramfile "${modp18}" \ 164 | -out "${msgdir}"/derive.pem 165 | 166 | # derive (large) shared secret 167 | openssl pkeyutl -derive -peerform der \ 168 | -inkey "${msgdir}"/derive.pem \ 169 | -peerkey "${msgdir}"/rpeer.der \ 170 | -out "${msgdir}"/shared.key 171 | 172 | # extract and sign ephemeral public peer key 173 | openssl pkey -pubout -outform der \ 174 | -in "${msgdir}"/derive.pem \ 175 | -out "${msgdir}"/speer.der 176 | 177 | openssl cms -sign -noattr -binary -md ${sigalg} -nodetach -nocerts -outform pem \ 178 | -signer "${certdir}"/verify.pem \ 179 | -inkey "${keysdir}"/sign.pem \ 180 | -in "${msgdir}"/speer.der \ 181 | -out "${msgdir}"/speer.sig.tmp 182 | mv -- "${msgdir}"/speer.sig.tmp "${msgdir}"/speer.sig 183 | 184 | 185 | # deterministically derive encryption and MAC keys from shared secret 186 | enckey=`openssl dgst -mac hmac -${enchash} -macopt key:encrypt "${msgdir}"/shared.key | cut -d' ' -f2` 187 | sendkey=`openssl dgst -mac hmac -${sigalg} -macopt key:send "${msgdir}"/shared.key | cut -d' ' -f2` 188 | recvkey=`openssl dgst -mac hmac -${sigalg} -macopt key:recv "${msgdir}"/shared.key | cut -d' ' -f2` 189 | [ ${#enckey} = 64 -a ${#sendkey} = 128 -a ${#recvkey} = 128 ] 190 | 191 | # compute message send/recv/ack MACs using derived MAC keys 192 | openssl dgst -mac hmac -${sigalg} \ 193 | -macopt hexkey:${sendkey} \ 194 | -out "${msgdir}"/send.mac \ 195 | "${msgdir}"/message 196 | 197 | openssl dgst -mac hmac -${sigalg} \ 198 | -macopt hexkey:${recvkey} \ 199 | -out "${msgdir}"/recv.mac \ 200 | "${msgdir}"/message 201 | 202 | openssl dgst -mac hmac -${sigalg} -macopt key:ack \ 203 | -out "${msgdir}"/ack.mac \ 204 | "${msgdir}"/shared.key 205 | 206 | sed -i 's/^.* //' -- "${msgdir}"/send.mac "${msgdir}"/recv.mac "${msgdir}"/ack.mac 207 | 208 | # encrypt message using encryption key 209 | openssl cms -EncryptedData_encrypt -binary -${encalg} -outform pem \ 210 | -secretkey ${enckey} \ 211 | -in "${msgdir}"/message \ 212 | -out "${msgdir}"/message.enc.tmp 213 | mv -- "${msgdir}"/message.enc.tmp "${msgdir}"/message.enc 214 | 215 | rm -- "${msgdir}"/derive.pem "${msgdir}"/speer.der "${msgdir}"/rpeer.der \ 216 | "${msgdir}"/shared.key 217 | 218 | ;; 219 | 220 | 221 | recv) 222 | rm -f -- "${msgdir}"/speer.der "${msgdir}"/shared.key "${msgdir}"/message \ 223 | "${msgdir}"/send.cmp "${msgdir}"/recv.mac "${msgdir}"/ack.mac 224 | 225 | # verify certificates chain 226 | verify_certs "${msgdir}" 227 | 228 | # verify and extract signed sender's ephemeral public peer key 229 | openssl cms -verify \ 230 | -x509_strict -policy_check -purpose smimesign -check_ss_sig -inform pem \ 231 | -CAfile "${msgdir}"/ca.pem -CApath /dev/null \ 232 | -certfile "${msgdir}"/verify.pem \ 233 | -in "${msgdir}"/speer.sig \ 234 | -out "${msgdir}"/speer.der 235 | 236 | # derive (large) shared secret 237 | openssl pkeyutl -derive -peerform der \ 238 | -inkey "${msgdir}"/derive.pem \ 239 | -peerkey "${msgdir}"/speer.der \ 240 | -out "${msgdir}"/shared.key 241 | 242 | # deterministically derive encryption and MAC keys from shared secret 243 | enckey=`openssl dgst -mac hmac -${enchash} -macopt key:encrypt "${msgdir}"/shared.key | cut -d' ' -f2` 244 | sendkey=`openssl dgst -mac hmac -${sigalg} -macopt key:send "${msgdir}"/shared.key | cut -d' ' -f2` 245 | recvkey=`openssl dgst -mac hmac -${sigalg} -macopt key:recv "${msgdir}"/shared.key | cut -d' ' -f2` 246 | [ ${#enckey} = 64 -a ${#sendkey} = 128 -a ${#recvkey} = 128 ] 247 | 248 | # decrypt message using encryption key 249 | openssl cms -EncryptedData_decrypt -inform pem \ 250 | -secretkey ${enckey} \ 251 | -in "${msgdir}"/message.enc \ 252 | -out "${msgdir}"/message 253 | 254 | # compute message send/recv/ack MACs using derived MAC keys 255 | openssl dgst -mac hmac -${sigalg} \ 256 | -macopt hexkey:${sendkey} \ 257 | -out "${msgdir}"/send.cmp \ 258 | "${msgdir}"/message 259 | 260 | openssl dgst -mac hmac -${sigalg} \ 261 | -macopt hexkey:${recvkey} \ 262 | -out "${msgdir}"/recv.mac \ 263 | "${msgdir}"/message 264 | 265 | openssl dgst -mac hmac -${sigalg} -macopt key:ack \ 266 | -out "${msgdir}"/ack.mac \ 267 | "${msgdir}"/shared.key 268 | 269 | sed -i 's/^.* //' -- "${msgdir}"/send.cmp "${msgdir}"/recv.mac "${msgdir}"/ack.mac 270 | 271 | # verify message MAC 272 | if ! cmp -s -- "${msgdir}"/send.mac "${msgdir}"/send.cmp; then 273 | error "MAC verification failed" 274 | fi 275 | 276 | rm -- "${msgdir}"/speer.der "${msgdir}"/shared.key "${msgdir}"/send.cmp 277 | 278 | ;; 279 | 280 | 281 | *) 282 | error "unknown command" 283 | ;; 284 | esac 285 | -------------------------------------------------------------------------------- /cable/comm: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | if [ $# != 2 ]; then 4 | echo "Format: $0 send|recv|ack|fin " 5 | exit 1 6 | fi 7 | 8 | 9 | # Directories and files 10 | username=`cat ${CABLE_CERTS}/certs/username | tr -cd a-z2-7` 11 | queue=${CABLE_QUEUES}/queue 12 | rqueue=${CABLE_QUEUES}/rqueue 13 | 14 | # Parameters 15 | cmd="$1" 16 | msgid="$2" 17 | 18 | 19 | trap '[ $? = 0 ] || error failed' 0 20 | error() { 21 | logger -t comm -p mail.err "$@ (${msgid})" 22 | trap - 0 23 | exit 1 24 | } 25 | 26 | 27 | urlprefix() { 28 | local username=`cat $1/"${msgid}"/username | tr -cd a-z2-7` 29 | local hostname=`cat $1/"${msgid}"/hostname | tr -cd '[:alnum:].-' | tr '[:upper:]' '[:lower:]'` 30 | check_userhost "${username}" "${hostname}" 31 | 32 | echo http://"${hostname}"/"${username}"/request 33 | } 34 | 35 | 36 | # MAC key extractor 37 | getmac() { 38 | local src="$1" mac= 39 | 40 | mac=`cat "${src}" | tr -cd '[:xdigit:]' | tr A-F a-f` 41 | [ ${#mac} = 128 ] || error "malformed or non-existing MAC in `basename "${src}"`" 42 | 43 | echo "${mac}" 44 | } 45 | 46 | 47 | # Sanity checks 48 | [ ${#msgid} = 40 ] || error "bad msgid" 49 | [ ${#username} = 32 ] || error "bad own username" 50 | 51 | check_userhost() { 52 | [ ${#1} = 32 ] || error "bad username" 53 | [ ${#2} != 0 ] || error "bad hostname" 54 | } 55 | 56 | 57 | case "${cmd}" in 58 | send) 59 | # [comm loop] 60 | if [ -e ${queue}/"${msgid}"/${cmd}.ok -a ! -e ${queue}/"${msgid}"/ack.ok ]; then 61 | prefix=`urlprefix ${queue}` 62 | sendmac=`getmac ${queue}/"${msgid}"/send.mac` 63 | curl -sSfg "${prefix}"/snd/"${msgid}"/"${sendmac}" 64 | else 65 | error "${cmd}.ok (without ack.ok) not found" 66 | fi 67 | ;; 68 | 69 | recv) 70 | # NOTE: dir can be renamed at any moment by [service] 71 | # [comm loop] 72 | if [ -e ${rqueue}/"${msgid}"/${cmd}.ok ]; then 73 | prefix=`urlprefix ${rqueue}` 74 | recvmac=`getmac ${rqueue}/"${msgid}"/recv.mac` 75 | curl -sSfg "${prefix}"/rcp/"${msgid}"/"${recvmac}" 76 | else 77 | error "${cmd}.ok not found" 78 | fi 79 | ;; 80 | 81 | ack) 82 | # [comm loop] 83 | if [ -e ${queue}/"${msgid}"/${cmd}.ok ]; then 84 | prefix=`urlprefix ${queue}` 85 | ackmac=`getmac ${queue}/"${msgid}"/ack.mac` 86 | curl -sSfg "${prefix}"/ack/"${msgid}"/"${ackmac}" 87 | 88 | mv -T ${queue}/"${msgid}" ${queue}/"${msgid}".del 89 | 90 | # try to run 2nd ack variant immediately 91 | # rm -r --one-file-system ${queue}/"${msgid}".del 92 | elif [ -e ${queue}/"${msgid}".del ]; then 93 | rm -r --one-file-system ${queue}/"${msgid}".del 94 | else 95 | error "${cmd}.ok or .del directory not found" 96 | fi 97 | ;; 98 | 99 | fin) 100 | # [comm loop] 101 | if [ -e ${rqueue}/"${msgid}".del ]; then 102 | rm -r --one-file-system ${rqueue}/"${msgid}".del 103 | else 104 | error ".del directory not found" 105 | fi 106 | ;; 107 | 108 | *) 109 | error "unknown command" 110 | ;; 111 | esac 112 | -------------------------------------------------------------------------------- /cable/crypto: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | if [ $# != 2 ]; then 4 | echo "Format: $0 send|peer|recv|ack " 5 | exit 1 6 | fi 7 | 8 | 9 | # Helpers 10 | cms=${CABLE_HOME}/cms 11 | mhdrop=${CABLE_HOME}/mhdrop 12 | 13 | 14 | # Directories 15 | inbox=${CABLE_INBOX} 16 | ssldir=${CABLE_CERTS} 17 | queue=${CABLE_QUEUES}/queue 18 | rqueue=${CABLE_QUEUES}/rqueue 19 | 20 | # Parameters 21 | cmd="$1" 22 | msgid="$2" 23 | 24 | 25 | trap '[ $? = 0 ] || error failed' 0 26 | error() { 27 | logger -t crypto -p mail.err "$@ (${msgid})" 28 | trap - 0 29 | exit 1 30 | } 31 | 32 | 33 | getuserhost() { 34 | username=`cat $1/"${msgid}"/username | tr -cd a-z2-7` 35 | hostname=`cat $1/"${msgid}"/hostname | tr -cd '[:alnum:].-' | tr '[:upper:]' '[:lower:]'` 36 | check_userhost "${username}" "${hostname}" 37 | } 38 | 39 | 40 | deliver() { 41 | local dir="$1" msg="$2" 42 | local grp=`stat -c %g "${dir}"` 43 | 44 | chgrp "${grp}" "${msg}" 45 | chmod 660 "${msg}" 46 | "${mhdrop}" "${dir}" "${msg}" 47 | } 48 | 49 | 50 | # Sanity checks 51 | [ ${#msgid} = 40 ] || error "bad msgid" 52 | 53 | check_userhost() { 54 | [ ${#1} = 32 ] || error "bad username" 55 | [ ${#2} != 0 ] || error "bad hostname" 56 | } 57 | 58 | 59 | case "${cmd}" in 60 | send) 61 | # [crypto loop] 62 | if [ -e ${queue}/"${msgid}"/${cmd}.rdy ]; then 63 | if "${cms}" ${cmd} ${ssldir} ${queue}/"${msgid}" 2>/dev/null; then 64 | mv ${queue}/"${msgid}"/${cmd}.rdy ${queue}/"${msgid}"/${cmd}.ok 65 | rm ${queue}/"${msgid}"/message ${queue}/"${msgid}"/ca.pem \ 66 | ${queue}/"${msgid}"/verify.pem ${queue}/"${msgid}"/rpeer.sig 67 | else 68 | mv ${queue}/"${msgid}"/${cmd}.rdy ${queue}/"${msgid}"/${cmd}.req 69 | error "${cmd} failed" 70 | fi 71 | else 72 | error "${cmd}.rdy not found" 73 | fi 74 | ;; 75 | 76 | peer) 77 | # [crypto loop] 78 | if [ -e ${rqueue}/"${msgid}"/${cmd}.req ]; then 79 | if "${cms}" ${cmd} ${ssldir} ${rqueue}/"${msgid}" 2>/dev/null; then 80 | mv ${rqueue}/"${msgid}"/${cmd}.req ${rqueue}/"${msgid}"/${cmd}.ok 81 | else 82 | error "${cmd} failed" 83 | fi 84 | else 85 | error "${cmd}.req not found" 86 | fi 87 | ;; 88 | 89 | recv) 90 | # [crypto loop] 91 | if [ -e ${rqueue}/"${msgid}"/${cmd}.rdy ]; then 92 | if "${cms}" ${cmd} ${ssldir} ${rqueue}/"${msgid}" 2>/dev/null; then 93 | getuserhost ${rqueue} 94 | date=`date -uR` 95 | 96 | if ! /bin/gzip -cdt ${rqueue}/"${msgid}"/message 2>/dev/null; then 97 | error "${cmd}: non-gzipped message" 98 | fi 99 | /bin/gzip -cd ${rqueue}/"${msgid}"/message \ 100 | | formail -z -I 'From ' \ 101 | -i "From: <${username}@${hostname}>" \ 102 | -I "X-Received-Date: ${date}" \ 103 | > ${rqueue}/"${msgid}"/message.ibx 104 | 105 | # Prepare headers for local MUA failure message 106 | formail -f -X From: -X To: -X Cc: -X Bcc: -X Subject: -X Date: \ 107 | -X Message-ID: -X In-Reply-To: -X References: -a 'Subject: ' \ 108 | < ${rqueue}/"${msgid}"/message.ibx | sed 's/^Subject: /&[fail] /i' \ 109 | > ${rqueue}/"${msgid}"/message.hdr 110 | 111 | deliver ${inbox} ${rqueue}/"${msgid}"/message.ibx 112 | 113 | mv ${rqueue}/"${msgid}"/${cmd}.rdy ${rqueue}/"${msgid}"/${cmd}.ok 114 | rm ${rqueue}/"${msgid}"/message ${rqueue}/"${msgid}"/message.enc \ 115 | ${rqueue}/"${msgid}"/ca.pem ${rqueue}/"${msgid}"/verify.pem \ 116 | ${rqueue}/"${msgid}"/derive.pem ${rqueue}/"${msgid}"/rpeer.sig \ 117 | ${rqueue}/"${msgid}"/speer.sig ${rqueue}/"${msgid}"/send.mac 118 | else 119 | rm ${rqueue}/"${msgid}"/send.mac 120 | mv ${rqueue}/"${msgid}"/${cmd}.rdy ${rqueue}/"${msgid}"/${cmd}.req 121 | error "${cmd} failed" 122 | fi 123 | else 124 | error "${cmd}.rdy not found" 125 | fi 126 | ;; 127 | 128 | ack) 129 | # [crypto loop] 130 | if [ -e ${queue}/"${msgid}"/${cmd}.req -a ! -e ${queue}/"${msgid}"/${cmd}.ok ]; then 131 | getuserhost ${queue} 132 | date=`date -uR` 133 | 134 | (formail -f -i "Date: ${date}" < ${queue}/"${msgid}"/message.hdr; \ 135 | echo "Verified message delivery to ${username}@${hostname}") \ 136 | > ${queue}/"${msgid}"/message.ack 137 | 138 | deliver ${inbox} ${queue}/"${msgid}"/message.ack 139 | 140 | mv ${queue}/"${msgid}"/${cmd}.req ${queue}/"${msgid}"/${cmd}.ok 141 | rm ${queue}/"${msgid}"/message.enc ${queue}/"${msgid}"/speer.sig \ 142 | ${queue}/"${msgid}"/send.mac ${queue}/"${msgid}"/recv.mac 143 | else 144 | error "${cmd}.req (without ${cmd}.ok) not found" 145 | fi 146 | ;; 147 | 148 | *) 149 | error "unknown command" 150 | ;; 151 | esac 152 | -------------------------------------------------------------------------------- /cable/fetch: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | if [ $# != 2 ]; then 4 | echo "Format: $0 send|recv " 5 | exit 1 6 | fi 7 | 8 | 9 | # Directories 10 | queue=${CABLE_QUEUES}/queue 11 | rqueue=${CABLE_QUEUES}/rqueue 12 | 13 | # Parameters 14 | cmd="$1" 15 | msgid="$2" 16 | 17 | 18 | trap '[ $? = 0 ] || error failed' 0 19 | error() { 20 | logger -t fetch -p mail.err "$@ (${msgid})" 21 | trap - 0 22 | exit 1 23 | } 24 | 25 | 26 | urlprefix() { 27 | local username=`cat $1/"${msgid}"/username | tr -cd a-z2-7` 28 | local hostname=`cat $1/"${msgid}"/hostname | tr -cd '[:alnum:].-' | tr '[:upper:]' '[:lower:]'` 29 | check_userhost "${username}" "${hostname}" 30 | 31 | echo http://"${hostname}"/"${username}" 32 | } 33 | 34 | 35 | # Sanity checks 36 | [ ${#msgid} = 40 ] || error "bad msgid" 37 | 38 | check_userhost() { 39 | [ ${#1} = 32 ] || error "bad username" 40 | [ ${#2} != 0 ] || error "bad hostname" 41 | } 42 | 43 | 44 | # Retry curl request for 400+ status codes 45 | retrycurl() { 46 | local status= delay= 47 | 48 | for delay in 0 5 10 15; do 49 | sleep ${delay} 50 | status=0; curl "$@" || status=$? 51 | 52 | if [ ${status} != 22 ]; then 53 | break 54 | fi 55 | done 56 | 57 | return ${status} 58 | } 59 | 60 | 61 | case "${cmd}" in 62 | send) 63 | # [fetch loop] 64 | if [ -e ${queue}/"${msgid}"/${cmd}.req ]; then 65 | prefix=`urlprefix ${queue}` 66 | 67 | susername=`cat ${queue}/"${msgid}"/susername | tr -cd '[:alnum:].-' | tr '[:upper:]' '[:lower:]'` 68 | shostname=`cat ${queue}/"${msgid}"/shostname | tr -cd '[:alnum:].-' | tr '[:upper:]' '[:lower:]'` 69 | check_userhost "${susername}" "${shostname}" 70 | 71 | curl -sSfg "${prefix}"/request/msg/"${msgid}"/"${shostname}"/"${susername}" 72 | 73 | # TODO: with precomputed rpeers, a delay will be much less likely 74 | retrycurl -sSfg -o ${queue}/"${msgid}"/rpeer.sig "${prefix}"/rqueue/"${msgid}".key 75 | 76 | # A multi-URI curl command doesn't fail on a bad early fetch 77 | curl -sSfg -o ${queue}/"${msgid}"/ca.pem "${prefix}"/certs/ca.pem 78 | curl -sSfg -o ${queue}/"${msgid}"/verify.pem "${prefix}"/certs/verify.pem 79 | 80 | mv ${queue}/"${msgid}"/${cmd}.req ${queue}/"${msgid}"/${cmd}.rdy 81 | else 82 | error "${cmd}.req not found" 83 | fi 84 | ;; 85 | 86 | recv) 87 | # [fetch loop] 88 | if [ -e ${rqueue}/"${msgid}"/${cmd}.req -a -e ${rqueue}/"${msgid}"/send.mac -a \ 89 | ! -e ${rqueue}/"${msgid}"/${cmd}.rdy -a ! -e ${rqueue}/"${msgid}"/${cmd}.ok ]; then 90 | prefix=`urlprefix ${rqueue}` 91 | 92 | # A multi-URI curl command deosn't fail on a bad early fetch 93 | curl -sSfg -o ${rqueue}/"${msgid}"/message.enc "${prefix}"/queue/"${msgid}" 94 | curl -sSfg -o ${rqueue}/"${msgid}"/speer.sig "${prefix}"/queue/"${msgid}".key 95 | curl -sSfg -o ${rqueue}/"${msgid}"/ca.pem "${prefix}"/certs/ca.pem 96 | curl -sSfg -o ${rqueue}/"${msgid}"/verify.pem "${prefix}"/certs/verify.pem 97 | 98 | mv ${rqueue}/"${msgid}"/${cmd}.req ${rqueue}/"${msgid}"/${cmd}.rdy 99 | else 100 | error "${cmd}.req (without .rdy/.ok) or send.mac not found" 101 | fi 102 | ;; 103 | 104 | *) 105 | error "unknown command" 106 | ;; 107 | esac 108 | -------------------------------------------------------------------------------- /cable/loop: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # This script ensures at most one running instance for a 4 | 5 | if [ $# -lt 2 -o \( queue != "$1" -a rqueue != "$1" \) ]; then 6 | echo "Format: $0 queue|rqueue [.del]" 7 | exit 1 8 | fi 9 | 10 | 11 | # Helpers 12 | validate=${CABLE_HOME}/validate 13 | fetch=${CABLE_HOME}/fetch 14 | crypto=${CABLE_HOME}/crypto 15 | comm=${CABLE_HOME}/comm 16 | 17 | # Parameters 18 | qtype=$1 19 | msgid="${2%.del}" 20 | dirid="${2}" 21 | 22 | lockmagick="$3" 23 | locktmout=2 24 | 25 | # Directories 26 | msgdir=${CABLE_QUEUES}/${qtype}/"${dirid}" 27 | 28 | 29 | trap '[ $? = 0 ] || error failed' 0 30 | error() { 31 | logger -t loop -p mail.err "$@ (${msgid})" 32 | trap - 0 33 | exit 1 34 | } 35 | 36 | 37 | # Sanity checks 38 | [ ${#msgid} = 40 ] || error "bad msgid" 39 | [ -r "${msgdir}" ] || error "cannot access" 40 | 41 | 42 | # .del actions: blocking lock (to let the renaming action finish) 43 | if [ "${dirid}" != "${msgid}" ]; then 44 | 45 | if [ ${qtype} = queue ]; then 46 | 47 | if [ -e "${msgdir}" ]; then 48 | exec flock -w ${locktmout} "${msgdir}"/ "${comm}" ack "${msgid}" 49 | else 50 | error ".del directory not found" 51 | fi 52 | 53 | else 54 | 55 | if [ -e "${msgdir}" ]; then 56 | exec flock -w ${locktmout} "${msgdir}"/ "${comm}" fin "${msgid}" 57 | else 58 | error ".del directory not found" 59 | fi 60 | 61 | fi 62 | 63 | else 64 | 65 | if [ ${qtype} = queue ]; then 66 | 67 | # lock combined operations 68 | if [ lockmagick != "${lockmagick}" ]; then 69 | exec flock -w ${locktmout} -n "${msgdir}"/ "$0" ${qtype} "${msgid}" lockmagick 70 | fi 71 | 72 | "${validate}" ${qtype} "${msgid}" 73 | set +e 74 | 75 | # handle ack first, to retry send if failed 76 | if [ -e "${msgdir}"/ack.ok ]; then 77 | "${comm}" ack "${msgid}" 78 | elif [ -e "${msgdir}"/ack.req ]; then 79 | "${crypto}" ack "${msgid}" && \ 80 | "${comm}" ack "${msgid}" 81 | fi 82 | 83 | # if ack succeeds, ${msgdir} is renamed 84 | if [ -e "${msgdir}" ]; then 85 | if [ -e "${msgdir}"/send.req ]; then 86 | "${fetch}" send "${msgid}" && \ 87 | "${crypto}" send "${msgid}" && \ 88 | exec "${comm}" send "${msgid}" 89 | elif [ -e "${msgdir}"/send.rdy ]; then 90 | "${crypto}" send "${msgid}" && \ 91 | exec "${comm}" send "${msgid}" 92 | elif [ -e "${msgdir}"/send.ok ]; then 93 | if [ ! -e "${msgdir}"/ack.ok ]; then 94 | exec "${comm}" send "${msgid}" 95 | fi 96 | else 97 | error "send.req/rdy/ok not found" 98 | fi 99 | fi 100 | 101 | else 102 | 103 | # lock combined operations 104 | if [ lockmagick != "${lockmagick}" ]; then 105 | exec flock -w ${locktmout} -n "${msgdir}"/ "$0" ${qtype} "${msgid}" lockmagick 106 | fi 107 | 108 | "${validate}" ${qtype} "${msgid}" 109 | set +e 110 | 111 | if [ -e "${msgdir}"/recv.rdy ]; then 112 | "${crypto}" recv "${msgid}" && \ 113 | "${comm}" recv "${msgid}" 114 | elif [ -e "${msgdir}"/recv.ok ]; then 115 | "${comm}" recv "${msgid}" 116 | elif [ -e "${msgdir}"/recv.req ]; then 117 | "${fetch}" recv "${msgid}" && \ 118 | "${crypto}" recv "${msgid}" && \ 119 | "${comm}" recv "${msgid}" 120 | fi 121 | 122 | if [ -e "${msgdir}"/peer.req ]; then 123 | exec "${crypto}" peer "${msgid}" 124 | elif [ ! -e "${msgdir}"/peer.ok ]; then 125 | error "peer.req/ok not found" 126 | fi 127 | 128 | fi 129 | 130 | fi 131 | -------------------------------------------------------------------------------- /cable/send: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # Setup environment with needed environment vars 4 | . /etc/cable/profile 5 | 6 | 7 | # Creates ${queuedir}/{username,hostname,message,send.req} 8 | queuedir=${CABLE_QUEUES}/queue 9 | 10 | msgidbytes=20 11 | emailregex="${CABLE_REGEX}" 12 | 13 | # Temporary directory on same filesystem 14 | tmpdir=`mktemp -d --tmpdir=${queuedir}` 15 | 16 | cleanup() { 17 | rm -r ${tmpdir} 18 | } 19 | 20 | trap 'st=$?; set +e; cleanup; exit ${st}' 0 21 | trap : INT QUIT TERM SEGV 22 | 23 | 24 | error() { 25 | # don't log via syslog, since invocation is interactive 26 | echo "send: $@" 1>&2 27 | exit 1 28 | } 29 | 30 | 31 | # Read the message on stdin, and remove X-*: headers 32 | formail -fz -I X- > ${tmpdir}/message 33 | 34 | 35 | # Prepare headers for local MUA confirmation message 36 | formail -f -X From: -X To: -X Cc: -X Bcc: -X Subject: -X Date: \ 37 | -X Message-ID: -X In-Reply-To: -X References: -a 'Subject: ' \ 38 | < ${tmpdir}/message | sed 's/^Subject: /&[vfy] /i' \ 39 | > ${tmpdir}/message.hdr 40 | 41 | 42 | # Extract bare x@y addresses 43 | addresses=`formail -fcz -x To: -x Cc: -x Bcc: < ${tmpdir}/message \ 44 | | tr , '\n' \ 45 | | sed 's/^.*<\([^>]*\)>/\1/' \ 46 | | sed 's/^[[:blank:]]\+//; s/[[:blank:]]\+$//' \ 47 | | tr '[:upper:]' '[:lower:]'` 48 | addresses=`echo ${addresses} | tr '[:blank:]' '\n' | sort -u` 49 | 50 | # Extract the bare From: address 51 | fromaddr=`formail -fcz -x From: < ${tmpdir}/message \ 52 | | sed 's/^.*<\([^>]*\)>/\1/' \ 53 | | tr '[:upper:]' '[:lower:]'` 54 | 55 | 56 | # Check address validity to prevent accidental information leaks 57 | for addr in "${fromaddr}" ${addresses}; do 58 | if ! echo x "${addr}" | egrep -q "^x ${emailregex}$"; then 59 | error "unsupported address: ${addr}" 60 | fi 61 | done 62 | 63 | 64 | # Extract the from-user and from-host 65 | echo ${fromaddr%@*} > ${tmpdir}/susername 66 | echo ${fromaddr#*@} > ${tmpdir}/shostname 67 | 68 | 69 | # Remove Bcc: header and replace Date: with a UTC one: 70 | date=`date -uR` 71 | formail -f -I Bcc: -I "Date: ${date}" < ${tmpdir}/message \ 72 | | gzip -c9n > ${tmpdir}/message.gz 73 | mv ${tmpdir}/message.gz ${tmpdir}/message 74 | 75 | 76 | # Create actual directories, one per addressee 77 | for addr in ${addresses}; do 78 | # generate and create as subdirectory 79 | msgid=`openssl rand -hex ${msgidbytes}` 80 | [ ${#msgid} = $((msgidbytes * 2)) ] || error "could not generate msgid" 81 | 82 | mkdir ${tmpdir}/${msgid} 83 | 84 | # extract username and hostname from address, and generate respective files 85 | echo ${addr%@*} > ${tmpdir}/${msgid}/username 86 | echo ${addr#*@} > ${tmpdir}/${msgid}/hostname 87 | 88 | # copy the sanitized message (assume the files will not be modified later) 89 | ln ${tmpdir}/message ${tmpdir}/message.hdr \ 90 | ${tmpdir}/susername ${tmpdir}/shostname \ 91 | ${tmpdir}/${msgid}/ 92 | 93 | # atomically move directory to queue dir, and create send.req indicator 94 | touch ${tmpdir}/${msgid}/send.req 95 | mv -T ${tmpdir}/${msgid} ${queuedir}/${msgid} 96 | done 97 | -------------------------------------------------------------------------------- /cable/validate: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | if [ $# != 2 -o \( queue != "$1" -a rqueue != "$1" \) ]; then 4 | echo "Format: $0 queue|rqueue " 5 | exit 1 6 | fi 7 | 8 | 9 | # Helpers 10 | mhdrop=${CABLE_HOME}/mhdrop 11 | 12 | 13 | # Parameters 14 | qtype=$1 15 | msgid="${2}" 16 | 17 | sectmout=${CABLE_TMOUT} 18 | subjrep='s/^\(Subject: \)\(\[vfy\] \)\?/\1[fail] /i' 19 | 20 | # Directories 21 | inbox=${CABLE_INBOX} 22 | msgdir=${CABLE_QUEUES}/${qtype}/"${msgid}" 23 | tsfile="${msgdir}"/username 24 | 25 | 26 | trap '[ $? = 0 ] || error failed' 0 27 | error() { 28 | logger -t validate -p mail.err "$@ (${msgid})" 29 | trap - 0 30 | exit 1 31 | } 32 | 33 | 34 | deliver() { 35 | local dir="$1" msg="$2" 36 | local grp=`stat -c %g "${dir}"` 37 | 38 | chgrp "${grp}" "${msg}" 39 | chmod 660 "${msg}" 40 | "${mhdrop}" "${dir}" "${msg}" 41 | } 42 | 43 | 44 | # Sanity checks 45 | [ ${#msgid} = 40 ] || error "bad msgid" 46 | [ -s "${tsfile}" ] || error "bad username file" 47 | 48 | check_userhost() { 49 | [ ${#1} = 32 ] || error "bad username" 50 | [ ${#2} != 0 ] || error "bad hostname" 51 | } 52 | 53 | 54 | # Determine if the message has timed out 55 | secstart=`stat -c %Y "${tsfile}"` 56 | secend=`date -u +%s` 57 | secdiff=$((secend - secstart)) 58 | 59 | if [ ${sectmout} -gt ${secdiff} ]; then 60 | exit 61 | fi 62 | 63 | 64 | # Variables 65 | date=`date -uR` 66 | username=`cat "${msgdir}"/username | tr -cd a-z2-7` 67 | hostname=`cat "${msgdir}"/hostname | tr -cd '[:alnum:].-' | tr '[:upper:]' '[:lower:]'` 68 | check_userhost "${username}" "${hostname}" 69 | 70 | 71 | # Classify message state and create an appropriate MUA notification 72 | if [ ${qtype} = queue ]; then 73 | # message.hdr is available, but [vfy] must be replaced with [fail] 74 | formail -f -i "Date: ${date}" < "${msgdir}"/message.hdr | sed "${subjrep}" 75 | 76 | if [ -e "${msgdir}"/ack.ok ]; then 77 | echo "Failed to acknowledge receipt received from ${username}@${hostname}" 78 | elif [ -e "${msgdir}"/send.ok ]; then 79 | echo "Failed to send message and receive receipt from ${username}@${hostname}" 80 | else 81 | echo "Failed to peer and encrypt message for ${username}@${hostname}" 82 | fi 83 | else 84 | if [ -e "${msgdir}"/recv.ok ]; then 85 | # message.hdr is available 86 | formail -f -i "Date: ${date}" < "${msgdir}"/message.hdr 87 | echo "Failed to send receipt and receive acknowledgment from ${username}@${hostname}" 88 | else 89 | # no message.hdr is available at this point 90 | msgdate=$(date -d "$(stat -c %y "${tsfile}")" -uR) 91 | formail -f -I "From: <${username}@${hostname}>" \ 92 | -I "Old-Date: ${msgdate}" -I "Date: ${date}" \ 93 | -I "Subject: [fail]" <&- 94 | 95 | if [ -e "${msgdir}"/recv.req -o -e "${msgdir}"/recv.rdy ]; then 96 | echo "Failed to fetch and decrypt message from ${username}@${hostname}" 97 | else 98 | echo "Failed to generate peer key for message from ${username}@${hostname}" 99 | fi 100 | fi 101 | fi > "${msgdir}"/message.rej 102 | 103 | 104 | # Deliver the reject message 105 | deliver ${inbox} "${msgdir}"/message.rej 106 | 107 | # Schedule message directory for removal 108 | mv -T "${msgdir}" "${msgdir}".del 109 | 110 | # Indicate unsuccessful status to caller 111 | exit 42 112 | -------------------------------------------------------------------------------- /conf/cabled: -------------------------------------------------------------------------------- 1 | #!/sbin/runscript 2 | 3 | description="Cables communication daemon" 4 | 5 | command=/usr/libexec/cable/cabled 6 | command_background="true" 7 | start_stop_daemon_args="-u cable" 8 | pidfile=/var/run/cabled.pid 9 | -------------------------------------------------------------------------------- /conf/extensions.cnf: -------------------------------------------------------------------------------- 1 | # Distinguished name is directly supplied to ssl-req 2 | 3 | [ req ] 4 | distinguished_name = req_empty 5 | 6 | [ req_empty ] 7 | 8 | 9 | # X.509 extensions 10 | # http://www.openssl.org/docs/apps/x509v3_config.html 11 | 12 | [ root ] 13 | 14 | subjectKeyIdentifier = hash 15 | authorityKeyIdentifier = keyid:always,issuer:always 16 | 17 | authorityInfoAccess = caIssuers;URI:http://dee.su/cables 18 | 19 | basicConstraints = critical, CA:true, pathlen:0 20 | keyUsage = critical, keyCertSign, cRLSign 21 | 22 | 23 | [ verify ] 24 | 25 | subjectKeyIdentifier = hash 26 | authorityKeyIdentifier = keyid:always,issuer:always 27 | 28 | keyUsage = critical, digitalSignature 29 | extendedKeyUsage = critical, emailProtection 30 | 31 | 32 | [ encrypt ] 33 | 34 | subjectKeyIdentifier = hash 35 | authorityKeyIdentifier = keyid:always,issuer:always 36 | 37 | keyUsage = critical, keyEncipherment 38 | extendedKeyUsage = critical, emailProtection 39 | -------------------------------------------------------------------------------- /conf/profile: -------------------------------------------------------------------------------- 1 | # Essential environment variables, to be sourced by 2 | # cables executables, possibly after "sudo -u " 3 | # Guaranteed environment: HOME, USER 4 | 5 | # rwX------ 6 | umask 077 7 | export LC_ALL=C 8 | 9 | 10 | # Base directories 11 | export CABLE_CONF=/etc/cable 12 | export CABLE_HOME=/usr/libexec/cable 13 | 14 | 15 | # Optional common root (not used outside this script) 16 | export CABLE_MOUNT=/home/anon/persist 17 | 18 | # Certificates and private keys directories 19 | export CABLE_CERTS=${CABLE_MOUNT}/security/cable 20 | export CABLE_TOR=${CABLE_MOUNT}/security/tor 21 | export CABLE_I2P=${CABLE_MOUNT}/security/i2p 22 | 23 | # CABLE_QUEUES/(r)queue directories, must be writable by uid 'cable' 24 | export CABLE_QUEUES=${CABLE_MOUNT}/cables 25 | 26 | # Mail delivery directory, must be writable by uid 'cable' 27 | export CABLE_INBOX=${CABLE_MOUNT}/mail/inbox 28 | 29 | 30 | # Message or receipt timeout in seconds (e.g., 7 days) 31 | export CABLE_TMOUT=$((7 * 24 * 60 * 60)) 32 | 33 | 34 | # Host and port on which cables daemon listens to HTTP connections 35 | # (symbolic names can be used; leave host empty for wildcard bind) 36 | export CABLE_HOST=127.0.0.1 37 | export CABLE_PORT=9080 38 | 39 | 40 | # Supported email-like IDs (do not modify!) 41 | export CABLE_REGEX='[a-z2-7]{32}@([a-z2-7]{16}\.onion|[a-z2-7]{52}\.b32\.i2p)' 42 | 43 | 44 | # Proxy settings (used by curl) 45 | export http_proxy=http://127.0.0.1:8118/ 46 | export https_proxy=${http_proxy} 47 | 48 | 49 | # Support pam_mktemp 50 | export TMPDIR=${TMPDIR:-/tmp/.private/${USER}} 51 | if [ ! -e "${TMPDIR}" ]; then 52 | TMPDIR=/tmp/.${USER} 53 | mkdir -pm 700 ${TMPDIR} 54 | fi 55 | 56 | 57 | # OpenSSL random seed 58 | export RANDFILE="${TMPDIR}"/openssl.rnd 59 | -------------------------------------------------------------------------------- /conf/rfc3526-modp-18.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN DH PARAMETERS----- 2 | MIIECAKCBAEA///////////JD9qiIWjCNMTGYouA3BzRKQJOCIpnzHQCC76mOxOb 3 | IlFKCHmONATd75UZs806QxswKwpt8l8UN0/hNW1tUcJF5IW1dmJefsb0TELppjft 4 | awv/XLb0Brft7jhr+1qJn6WunyQRfEsf5kkoZlHs5Fs9wgB8uKFjvwWY2kg2HFXT 5 | mmkWP6j9JM9fg2VdI9yjrZYcYvNWIIVSu57VKQdwlpZtZww1Tkq8mATxdGwIyhgh 6 | fDKQXkYuNs474553LBgOhgObJ4Oi7Aeij7XFXfBvTFLJ3ivL9pVYFxg5lUl86pVq 7 | 5RXSJhiY+gUQFXKOWoqqxC2tMxcNBFB6M6hVIavfHLpk7PuFBFjb7wqK6nFXXQYM 8 | fbOXD4Wm4eTHq/WujNsJM9cejJTgSiVhnc7j0iYa0u5r8S/6BtmKCGTYdgJzPshq 9 | ZFIfKxgXeyAMu+EXV3phXWx3CYjAutlG4gjiT6B05asxQ9tb/OD9EI5LgtEgqSEI 10 | ARpyPBKnh+bXiHGaEL26WyaZwycYavTiPBqUaDS2FQvaJYPpyirUTOjbu8LbBN6O 11 | +S6O/BQfvsqmKHxZR05rwF2ZspZPoJDDoiM7oYZRW+ftH2EpcM7i16+4G912IXBI 12 | HNAGkSfVsFqpk7TqmI2P3cGG/7fckKbAj030Nck0AoSSNsP6tNJ8cCbB1NyyYCZG 13 | 3sl1HnY9uje9+P+UBq2eUw7l2zgvQTABrrBqU+2QJ9gxF5cnsIZaiRjaPtvrz5sU 14 | 7UTObLrO1Lsb238UR+bMJUszIFFRK9evQm+49AE3jNK/WYPKAcZLkuzwMuoV0XId 15 | A/SC185udP721V5wL0aYDIK1qEAxkAscnlnnyX++x+jzI6l6fjbMiL4PHUW3/1ha 16 | xUvUB7IrQVSqzI9tfr9I4dgUzF7SD4A34KeXFe7ym+MoBqHVi7fF2nb1UKo9ih+/ 17 | 8OsZzLGjE9Vc2lbJ7C7yljI4f+jXbjwEaAQ+j2Y/SGDuEr8tWwt0dNbmlPkebb4R 18 | WXSjkm8S/uXkOHd8tqky34zYvsTQc7kxujvIMraNndMAdB+nv4r8R+0ldvaTa6Qk 19 | ZjqrY5xa5PVoNCO0dCvxyXgjjxbL451lLeP9uL78hIrZIiIuBKQDfAcT61eoGiPw 20 | xzRz/GRs6jBrS8vIhi+Dhd36nUt/osCH6HloMwPtW906Bis89bOieKZtKhP4P0T4 21 | Ld8xDuB0q2o2RZfomaAlXcFk8xzFCEaFHfmrSBld7X6hsdUQvX7nTXP682vDHs+i 22 | aDWQRvTrh5+SQAlDi0gcbNeImgAu1e44K8kZDab8Am5HlVjkR1Z36aqeMFDidlaU 23 | 38gfVuiAuW5xYMmA3Zjt09///////////wIBAg== 24 | -----END DH PARAMETERS----- 25 | -------------------------------------------------------------------------------- /doc/cable.txt: -------------------------------------------------------------------------------- 1 | Message exchange with PFS and repudiability 2 | =========================================== 3 | 4 | Authentication and encryption 5 | ----------------------------- 6 | 7 | Permanent keys: 8 | + public X.509 [ca.pem] (root CA) + username (hash of [ca.pem]) 9 | + public X.509 [verify.pem] (issued by root CA) 10 | + private key [sign.pem] (corresponding to X.509 [verify.pem]) 11 | 12 | Ephemeral keys: 13 | + public DH key [speer.sig or rpeer.sig] (signed, verifiable with [verify.pem]) 14 | + private DH key [derive.pem] (corresponding to DH [s/rpeer.pem]) 15 | + shared [shared.key] 16 | 17 | Key derivation: 18 | + sender's ephemeral DH key [speer.der] or private [derive.pem] 19 | + recipient's ephemeral DH key [rpeer.der] or private [derive.pem] 20 | + MAC_{send,recv,ack} keys are different hash functions derived from [shared.key] 21 | (ephemeral-ephemeral key agreement: http://tools.ietf.org/html/rfc2785) 22 | 23 | (sender) 24 | + [message] <- 25 | + [peer request] -> 26 | 27 | (recipient) 28 | + [rpeer.der] -> signed with recipient's [sign.pem] -> [rpeer.sig] 29 | + [rpeer.sig] -> 30 | 31 | (contd., sender) 32 | + [rpeer.sig] <- 33 | + [rpeer.sig] -> verified with recipient's [verify.pem] -> [rpeer.der] 34 | + [derive.pem] + [rpeer.der] -> DH key derivation -> [shared.key] 35 | + [speer.der] -> signed with sender's [sign.pem] -> [speer.sig] 36 | + [message] -> encrypted with shared [shared.key] -> [message.enc] 37 | + [message.enc] + [speer.sig] + MAC_send([message]) -> 38 | 39 | (recipient) 40 | + [message.enc] + [speer.sig] + MAC_send(message) <- 41 | + [speer.sig] -> verified with sender's [verify.pem] -> [speer.der] 42 | + [derive.pem] + [speer.der] -> DH key derivation -> [shared.key] 43 | + [message.enc] -> decrypted with shared [shared.key] -> [message] 44 | + [message] -> verified with shared MAC_send 45 | + MAC_recv([message]) -> 46 | 47 | (sender) 48 | + MAC_recv([message]) <- 49 | + [message] -> verified with shared MAC_recv 50 | + MAC_ack -> 51 | 52 | (recipient) 53 | + MAC_ack <- 54 | + MAC_ack -> compared with shared MAC_ack 55 | 56 | 57 | Protocol 58 | -------- 59 | + username is explicitly verified against public key fingerprint 60 | + hostname is implicitly verified when fetching files 61 | + perfect forward secrecy and repudiability 62 | + resistant against MITM injections (except first MSG substitution with same ) 63 | + resistant against temporary MITM resources substitution 64 | + resistant to request replay attacks intended to cause large number of disk writes 65 | 66 | + resistant against fingerprinting if username is unknown 67 | + vulnerable to DoS (e.g., many MSG requests) if username is known 68 | 69 | + each loop type is mutually exclusive for a given (r)queue/ 70 | + all code blocks are restartable (e.g., after crash) 71 | + messages and confirmations are never lost if /cables filesystem is transactional 72 | 73 | + /cables/ private directory 74 | /queue// outgoing message work dir 75 | /rqueue// incoming message work dir 76 | 77 | + [send] (MUA-invoked script) writes to /cables/queue 78 | + [service] (fast and secure web service) writes to /cables/(r)queue 79 | + [crypto loop] writes to /cables/(r)queue, MUA inbox directory; 80 | reads from certs (public, private) directories 81 | + [fetch loop] writes to /cables/(r)queue, network 82 | + [comm loop] writes to network; 83 | reads username from certs directory 84 | + [webserver] writes to network; 85 | reads from certs public directory, /cables/(r)queue 86 | 87 | 88 | [webserver] 89 | + / common URL prefix 90 | + /certs/{ca,verify}.pem serve public certificates 91 | + /queue/ serve /cables/queue//message.enc 92 | + /queue/.key serve /cables/queue//speer.sig 93 | + /rqueue/.key serve /cables/rqueue//rpeer.sig 94 | + /request/... invoke service[...] and serve answer 95 | 96 | 97 | (sender) 98 | [send] 99 | + generate random 160-bit (40 hex digits) 100 | + prepare /cables/queue//{message{,.hdr},{,s}{user,host}name} 101 | + create /cables/queue//send.req 102 | * (atomic via rename from /cables/queue/tmp.//) 103 | 104 | [fetch loop] 105 | + check /cables/queue//send.req 106 | + request //request/msg/// 107 | + fetch //rqueue/.key -> /cables/queue//rpeer.sig 108 | + fetch //certs/{ca,verify}.pem -> /cables/queue// 109 | + rename /cables/queue//send.req -> send.rdy 110 | 111 | [crypto loop] 112 | + check /cables/queue//send.rdy 113 | + prepare /cables/queue//{speer.sig[atomic],message.enc[atomic],{send,recv,ack}.mac} 114 | + rename /cables/queue//send.rdy -> send.ok (success) 115 | + -> send.req (crypto fail) 116 | + remove /cables/queue//{message,{ca,verify}.pem,rpeer.sig} (if success) 117 | 118 | [comm loop] 119 | + check /cables/queue//send.ok 120 | + checkno /cables/queue//ack.ok 121 | + read /cables/queue//send.mac (128 hex digits) 122 | + request //request/snd// 123 | 124 | 125 | (recipient) 126 | [service] 127 | + upon msg/// 128 | + checkno /cables/rqueue/ (ok and skip if exists) 129 | + create /cables/rqueue/.new/ (ok if exists) 130 | + write /cables/rqueue/.new/{username,hostname} 131 | + create /cables/rqueue/.new/peer.req (ok if exists) 132 | + rename /cables/rqueue/.new -> 133 | 134 | [crypto loop] 135 | + check /cables/rqueue//peer.req 136 | + prepare /cables/rqueue//{derive.pem,rpeer.sig[atomic]} 137 | + rename /cables/rqueue//peer.req -> peer.ok (success) 138 | 139 | 140 | (recipient) 141 | [service] 142 | + upon snd// 143 | + check /cables/rqueue//peer.ok 144 | + write /cables/rqueue//send.mac (skip if exists) 145 | + create /cables/rqueue//recv.req (atomic, ok if exists) 146 | + touch /cables/rqueue// (if recv.req did not exist) 147 | 148 | [fetch loop] 149 | + check /cables/rqueue//recv.req 150 | + check /cables/rqueue//send.mac 151 | + checkno /cables/rqueue//recv.{rdy,ok} (in this order) 152 | + fetch //queue/ -> /cables/rqueue//message.enc 153 | + fetch //queue/.key -> /cables/rqueue//speer.sig 154 | + fetch //certs/{ca,verify}.pem -> /cables/rqueue// 155 | + rename /cables/rqueue//recv.req -> recv.rdy 156 | 157 | [crypto loop] 158 | + check /cables/rqueue//recv.rdy 159 | + prepare /cables/rqueue//{message{,.hdr},{recv,ack}.mac} 160 | + verify /cables/rqueue//send.mac (remove if fail) 161 | + create <- /cables/rqueue//message 162 | + rename /cables/rqueue//recv.rdy -> recv.ok (success) 163 | + -> recv.req (crypto fail) 164 | + remove /cables/rqueue//{message{,.enc},{ca,verify,derive}.pem,{r,s}peer.sig,send.mac} (if success) 165 | 166 | [comm loop] 167 | + check /cables/rqueue//recv.ok 168 | + read /cables/rqueue//recv.mac (128 hex digits) 169 | + request //request/rcp// 170 | 171 | 172 | (sender) 173 | [service] 174 | + upon rcp// 175 | + check /cables/queue//send.ok 176 | + compare /cables/queue//recv.mac <-> 177 | + create /cables/queue//ack.req (atomic, ok if exists) 178 | + touch /cables/queue// (if ack.req did not exist) 179 | 180 | [crypto loop] 181 | + check /cables/queue//ack.req 182 | + checkno /cables/queue//ack.ok 183 | + create 184 | + rename /cables/queue//ack.req -> ack.ok 185 | + remove /cables/queue//{message.enc,speer.sig,{send,recv}.mac} (if success) 186 | 187 | [comm loop] 188 | + check /cables/queue//ack.ok 189 | + read /cables/queue//ack.mac (128 hex digits) 190 | + request //request/ack// (wait for good response) 191 | + rename /cables/queue/ -> .del 192 | (if ack is not handled or lost due to MITM attack, recipient will keep requesting rcp//) 193 | 194 | -and/or- 195 | 196 | + check /cables/queue/.del/ 197 | + remove /cables/queue/.del/ 198 | 199 | 200 | (recipient) 201 | [service] 202 | + upon ack// 203 | + check /cables/rqueue//recv.ok 204 | + compare /cables/rqueue//ack.mac <-> 205 | + rename /cables/rqueue/ -> .del 206 | 207 | [comm loop] 208 | + check /cables/rqueue/.del/ 209 | + remove /cables/rqueue/.del/ 210 | 211 | 212 | Loop scheduler 213 | -------------- 214 | 215 | Initialization (for s of 40 hex digits): 216 | + remove /cables/rqueue/.new/ (before [service] startup) 217 | 218 | Watch list (for s of 40 hex digits): 219 | + /cables/queue/ , .del (inotify: moved_to, attrib) 220 | + /cables/rqueue/ , .del (inotify: moved_to, attrib) 221 | 222 | + [service]: non-blocking lock attempt 223 | + [loop]: blocking lock (to let renaming actions complete, with short timeout) 224 | 225 | Retry policies: 226 | + retry every X min. (+ random component) 227 | 228 | Validation: upon reaching max age (from /username timestamp): 229 | (mutually exclusive with all loop types) 230 | 231 | + (queue) create 232 | [if] ack.ok: failed to acknowledge receipt 233 | [elif] send.ok: failed to send message and receive receipt 234 | [else] failed to peer and encrypt message 235 | + (queue) rename /cables/queue/ -> .del 236 | 237 | + (rqueue) create 238 | [if] recv.ok: failed to send receipt and receive acknowledgment 239 | [elif] recv.req/rdy: failed to fetch and decrypt message 240 | [else] failed to generate peer key 241 | + (rqueue) rename /cables/rqueue/ -> .del 242 | 243 | 244 | Message format 245 | -------------- 246 | 247 | (send) 248 | + extract all unique To:, Cc:, Bcc: addresses 249 | + check that all addresses (+ From:) are recognized (e.g., *.onion) 250 | + remove Bcc: and all X-*: headers 251 | + reformat Date: as UTC 252 | + compress with gzip 253 | 254 | 255 | (recv) 256 | + uncompress with classic (single-threaded) gzip 257 | + replace From: header with the verified address (rename old header) 258 | + add X-Received-Date: header 259 | 260 | 261 | (ack) 262 | + extract original From:, To:, Cc:, Bcc:, Subject:, Date:, Message-ID:, 263 | In-Reply-To:, References: fields 264 | + replace Date: header with current date (rename old header) 265 | + prepend [vfy] to Subject: field contents 266 | + append body with verification message, including current timestamp 267 | and verified delivery address 268 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | # Single-source file programs to build 2 | progs = cable/daemon cable/mhdrop cable/hex2base32 \ 3 | $(if $(NOI2P),,cable/eeppriv.jar) 4 | objextra_daemon = obj/server.o obj/service.o obj/process.o obj/util.o 5 | ldextra_daemon = -lrt -lmicrohttpd 6 | cpextra_EepPriv = /opt/i2p/lib/i2p.jar 7 | 8 | title := $(shell grep -o 'LIBERTE CABLE [[:alnum:]._-]\+' src/daemon.h) 9 | 10 | 11 | # Installation directories (override DESTDIR and/or PREFIX) 12 | # (DESTDIR is temporary, (ETC)PREFIX is hard-coded into scripts) 13 | DESTDIR= 14 | PREFIX=/usr 15 | ETCPREFIX=$(patsubst %/usr/etc,%/etc,$(PREFIX)/etc) 16 | 17 | instdir=$(DESTDIR)$(PREFIX) 18 | etcdir=$(DESTDIR)$(ETCPREFIX) 19 | 20 | 21 | # Default compilers 22 | CC = gcc 23 | JAVAC = javac 24 | 25 | # Disable I2P eepSite keypair generation functionality? (non-empty to disable) 26 | NOI2P = 27 | 28 | # Modifications to compiler flags 29 | CFLAGS := -std=c99 -Wall -pedantic -MMD -D_FILE_OFFSET_BITS=64 -D_POSIX_C_SOURCE=200809L -D_BSD_SOURCE -DNDEBUG $(CFLAGS) 30 | JFLAGS := -target 1.5 -deprecation -Werror -g:none $(JFLAGS) 31 | JLIBS = $(subst : ,:,$(addsuffix :,$(wildcard lib/*.jar))) 32 | 33 | 34 | # Build rules 35 | .PHONY: all clean install 36 | .SUFFIXES: .o 37 | .SECONDARY: 38 | 39 | all: $(progs) 40 | 41 | clean: 42 | $(RM) -r $(progs) obj/* 43 | 44 | cable/%: obj/%.o 45 | $(CC) -o $@ $(CFLAGS) $< $(objextra_$*) $(LDFLAGS) $(ldextra_$*) 46 | 47 | obj/%.o: src/%.c 48 | $(CC) -c -o $@ $(CFLAGS) $< 49 | 50 | cable/eeppriv.jar: obj/su/dee/i2p/EepPriv.class 51 | echo "Manifest-Version: 1.0" > $(> $(> $(> $(&$(PREFIX)/libexec/cable&g' \ 67 | $(addprefix $(etcdir)/cable/,profile cabled) \ 68 | $(instdir)/bin/cable-send 69 | sed -i 's&/etc/cable\>&$(ETCPREFIX)/cable&g' \ 70 | $(etcdir)/cable/profile \ 71 | $(addprefix $(instdir)/libexec/cable/,cabled send) \ 72 | $(addprefix $(instdir)/bin/,cable-id cable-ping cable-send gen-cable-username gen-tor-hostname gen-i2p-hostname) 73 | ifeq ($(strip $(NOI2P)),) 74 | chmod a-x $(instdir)/libexec/cable/eeppriv.jar 75 | else 76 | rm $(instdir)/bin/gen-i2p-hostname 77 | endif 78 | 79 | 80 | # File-specific dependencies 81 | cable/daemon: $(objextra_daemon) 82 | -include $(wildcard obj/*.d) 83 | -------------------------------------------------------------------------------- /pkg/cables-x.y.ebuild: -------------------------------------------------------------------------------- 1 | # Copyright 1999-2012 Gentoo Foundation 2 | # Distributed under the terms of the GNU General Public License v2 3 | # $Header: $ 4 | 5 | EAPI="4" 6 | 7 | # no mime types, so no need to inherit fdo-mime 8 | inherit eutils user 9 | 10 | DESCRIPTION="Secure and anonymous serverless email-like communication." 11 | HOMEPAGE="http://dee.su/cables" 12 | LICENSE="GPL-2" 13 | 14 | # I2P version is not critical, and need not be updated 15 | MY_P_PF=mkdesu-cables 16 | I2P_PV=0.8.8 17 | I2P_MY_P=i2pupdate_${I2P_PV} 18 | 19 | # GitHub URI can refer to a tagged download or the master branch 20 | SRC_URI="https://github.com/mkdesu/cables/tarball/v${PV} -> ${P}.tar.gz 21 | i2p? ( 22 | http://mirror.i2p2.de/${I2P_MY_P}.zip 23 | http://launchpad.net/i2p/trunk/${I2P_PV}/+download/${I2P_MY_P}.zip 24 | )" 25 | 26 | SLOT="0" 27 | KEYWORDS="x86 amd64" 28 | IUSE="i2p" 29 | 30 | DEPEND="app-arch/unzip 31 | i2p? ( >=virtual/jdk-1.5 )" 32 | RDEPEND="net-libs/libmicrohttpd 33 | mail-filter/procmail 34 | net-misc/curl 35 | dev-libs/openssl 36 | i2p? ( >=virtual/jre-1.5 ) 37 | gnome-extra/zenity" 38 | 39 | src_unpack() { 40 | unpack ${P}.tar.gz 41 | mv ${MY_P_PF}-* ${P} || die "failed to recognize archive top directory" 42 | 43 | if use i2p; then 44 | unzip -j -d ${P}/lib ${DISTDIR}/${I2P_MY_P}.zip lib/i2p.jar || die "failed to extract i2p.jar" 45 | fi 46 | } 47 | 48 | src_prepare() { 49 | if ! use i2p; then 50 | export MAKEOPTS+=" NOI2P=1" 51 | fi 52 | 53 | default 54 | } 55 | 56 | src_install() { 57 | default 58 | 59 | doinitd "${D}"/etc/cable/cabled 60 | rm "${D}"/etc/cable/cabled || die 61 | } 62 | 63 | pkg_preinst() { 64 | enewgroup cable 65 | enewuser cable -1 -1 -1 cable 66 | } 67 | 68 | pkg_postinst() { 69 | elog "Remember to add 'cabled' to the default runlevel." 70 | elog "You need to adjust the user-specific paths in /etc/cable/profile." 71 | elog "Generate cables certificates and Tor/I2P keypairs for the user:" 72 | elog " gen-cable-username" 73 | elog " gen-tor-hostname" 74 | elog " copy CABLE_TOR/hidden_service to /var/lib/tor (readable only by 'tor')" 75 | if use i2p; then 76 | elog " gen-i2p-hostname" 77 | elog " copy CABLE_I2P/eepsite to /var/lib/i2p (readable only by 'i2p')" 78 | fi 79 | elog "Configure Tor to forward HTTP connections to cables daemon:" 80 | elog " /etc/tor/torrc" 81 | elog " HiddenServiceDir /var/lib/tor/hidden_service/" 82 | elog " HiddenServicePort 80 127.0.0.1:9080" 83 | if use i2p; then 84 | elog "Configure I2P similarly:" 85 | elog " /var/lib/i2p/i2ptunnel.config" 86 | elog " tunnel.X.privKeyFile=eepsite/eepPriv.dat" 87 | elog " tunnel.X.targetHost=127.0.0.1" 88 | elog " tunnel.X.targetPort=9080" 89 | fi 90 | elog "Finally, the user should configure the email client to run cable-send" 91 | elog "as a pipe for sending messages from addresses shown by cable-info." 92 | elog "See comments in /usr/bin/cable-send for suggested /etc/sudoers entry." 93 | } 94 | -------------------------------------------------------------------------------- /share/cable-info.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Cables Identity 3 | Comment=Show user identity for secure cables communication 4 | Exec=cable-info 5 | Terminal=false 6 | Type=Application 7 | Categories=GTK;Network;Email; 8 | Icon=emblem-mail 9 | -------------------------------------------------------------------------------- /src/daemon.c: -------------------------------------------------------------------------------- 1 | /* 2 | The following environment variables are used (from /etc/cable/profile): 3 | CABLE_HOME, CABLE_QUEUES, CABLE_CERTS, CABLE_HOST, CABLE_PORT 4 | 5 | Testing environment: 6 | CABLE_NOLOOP, CABLE_NOWATCH 7 | */ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | #include "daemon.h" 23 | #include "server.h" 24 | #include "process.h" 25 | #include "util.h" 26 | 27 | 28 | /* environment variables */ 29 | #define CABLE_HOME "CABLE_HOME" 30 | #define CABLE_QUEUES "CABLE_QUEUES" 31 | #define CABLE_CERTS "CABLE_CERTS" 32 | #define CABLE_HOST "CABLE_HOST" 33 | #define CABLE_PORT "CABLE_PORT" 34 | 35 | /* executables and subdirectories */ 36 | #define LOOP_NAME "loop" 37 | #define QUEUE_NAME "queue" 38 | #define RQUEUE_NAME "rqueue" 39 | #define CERTS_NAME "certs" 40 | 41 | 42 | /* waiting strategy for inotify setup retries (e.g., after fs unmount) */ 43 | #define WAIT_INIT 2 44 | #define WAIT_MULT 1.5 45 | #define WAIT_MAX 60 46 | 47 | /* 48 | retry and limits strategies 49 | wait time for too many processes can be long, since SIGCHLD interrupts sleep 50 | */ 51 | #ifndef TESTING 52 | #define RETRY_TMOUT 150 53 | #define MAX_PROC 100 54 | #define WAIT_PROC 300 55 | #else 56 | #define RETRY_TMOUT 5 57 | #define MAX_PROC 5 58 | #define WAIT_PROC 5 59 | #endif 60 | 61 | /* inotify mask for for (r)queue directories */ 62 | #define INOTIFY_MASK (IN_ATTRIB | IN_MOVED_TO | IN_MOVE_SELF | IN_DONT_FOLLOW | IN_ONLYDIR) 63 | 64 | 65 | /* inotify file descriptor and (r)queue directories watch descriptors */ 66 | static int inotfd = -1, inotqwd = -1, inotrqwd = -1; 67 | 68 | 69 | /* unregister inotify watches and dispose of allocated file descriptor */ 70 | static void unreg_watches() { 71 | if (inotfd != -1) { 72 | /* ignore errors due to automatically removed watches (IN_IGNORED) */ 73 | if (inotqwd != -1) { 74 | if (inotify_rm_watch(inotfd, inotqwd) && errno != EINVAL) 75 | warning("failed to remove inotify watch"); 76 | inotqwd = -1; 77 | } 78 | 79 | /* ignore errors due to automatically removed watches (IN_IGNORED) */ 80 | if (inotrqwd != -1) { 81 | if (inotify_rm_watch(inotfd, inotrqwd) && errno != EINVAL) 82 | warning("failed to remove inotify watch"); 83 | inotrqwd = -1; 84 | } 85 | 86 | /* 87 | closing/reopening an inotify fd is an expensive operation, but must be done 88 | because otherwise fd provides infinite stream of IN_IGNORED events 89 | */ 90 | if (close(inotfd)) 91 | warning("could not close inotify fd"); 92 | inotfd = -1; 93 | } 94 | } 95 | 96 | 97 | /* register (r)queue-specific inotify watches, returning 1 if successful */ 98 | static int reg_watches(const char *qpath, const char *rqpath) { 99 | /* don't block on read(), since select() is used for polling */ 100 | if (inotfd == -1 && (inotfd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC)) == -1) { 101 | warning("failed to initialize inotify instance"); 102 | return 0; 103 | } 104 | 105 | #ifdef TESTING 106 | if (getenv("CABLE_NOWATCH")) 107 | return 1; 108 | #endif 109 | 110 | /* existing watch is ok */ 111 | if ((inotqwd = inotify_add_watch(inotfd, qpath, INOTIFY_MASK)) == -1) { 112 | warning("could not add inotify watch"); 113 | return 0; 114 | } 115 | 116 | /* existing watch is ok */ 117 | if ((inotrqwd = inotify_add_watch(inotfd, rqpath, INOTIFY_MASK)) == -1) { 118 | warning("could not add inotify watch"); 119 | return 0; 120 | } 121 | 122 | return 1; 123 | } 124 | 125 | 126 | /* 127 | try to register inotify watches, unregistering them if not compeltely successful 128 | hold an open fd during the attempt, to prevent unmount during the process 129 | */ 130 | static int try_reg_watches(const char *qpath, const char *rqpath) { 131 | int mpfd, ret = 0; 132 | struct stat st; 133 | 134 | /* unregister existing inotify watches */ 135 | unreg_watches(); 136 | 137 | /* try to quickly open a fd (expect read access on qpath) */ 138 | if ((mpfd = open(qpath, O_RDONLY | O_NONBLOCK | O_CLOEXEC)) != -1) { 139 | if (lstat(qpath, &st) == -1 || !S_ISDIR(st.st_mode)) 140 | flog(LOG_NOTICE, "%s is not a directory, waiting...", qpath); 141 | else if (lstat(rqpath, &st) == -1 || !S_ISDIR(st.st_mode)) 142 | flog(LOG_NOTICE, "%s is not a directory, waiting...", rqpath); 143 | 144 | /* if registering inotify watches is unsuccessful, immediately unregister */ 145 | else if (reg_watches(qpath, rqpath)) 146 | ret = 1; 147 | else 148 | unreg_watches(); 149 | 150 | 151 | /* free the pin fd */ 152 | if (close(mpfd)) 153 | warning("could not close pin directory"); 154 | } 155 | else 156 | flog(LOG_NOTICE, "failed to pin %s, waiting...", qpath); 157 | 158 | return ret; 159 | } 160 | 161 | 162 | /* retry registering inotify watches, using the retry strategy parameters */ 163 | static void wait_reg_watches(const char *qpath, const char *rqpath) { 164 | double slp = WAIT_INIT; 165 | 166 | while (!stop_requested()) { 167 | if (try_reg_watches(qpath, rqpath)) { 168 | flog(LOG_DEBUG, "registered watches"); 169 | break; 170 | } 171 | else { 172 | sleepsec(slp); 173 | 174 | slp = (slp * WAIT_MULT); 175 | if (slp > WAIT_MAX) 176 | slp = WAIT_MAX; 177 | } 178 | } 179 | } 180 | 181 | 182 | /* lower-case hexadecimal of correct length, possibly ending with ".del" */ 183 | static int is_msgdir(char *s) { 184 | size_t len = strlen(s); 185 | int res = 0; 186 | 187 | if (len == MSGID_LENGTH) 188 | res = vfyhex(MSGID_LENGTH, s); 189 | 190 | else if (len == MSGID_LENGTH+4 && strcmp(".del", s + MSGID_LENGTH) == 0) { 191 | s[MSGID_LENGTH] = '\0'; 192 | res = vfyhex(MSGID_LENGTH, s); 193 | s[MSGID_LENGTH] = '.'; 194 | } 195 | 196 | return res; 197 | } 198 | 199 | 200 | /* run loop for given queue type and msgid[.del]; msgid is a volatile string */ 201 | static void run_loop(const char *qtype, const char *msgid, const char *looppath) { 202 | const char *args[] = { looppath, qtype, msgid, NULL }; 203 | 204 | if (run_process(MAX_PROC, WAIT_PROC, args)) 205 | flog(LOG_INFO, "processing: %s %s", qtype, msgid); 206 | else 207 | flog(LOG_WARNING, "failed to launch: %s %s", qtype, msgid); 208 | } 209 | 210 | 211 | /* return whether an event is ready on the inotify fd, with timeout */ 212 | static int wait_read(int fd, double sec) { 213 | struct timeval tv; 214 | fd_set rfds; 215 | int ret; 216 | 217 | /* support negative arguments */ 218 | if (sec < 0) 219 | sec = 0; 220 | 221 | tv.tv_sec = (time_t) sec; 222 | tv.tv_usec = (suseconds_t) ((sec - tv.tv_sec) * 1e6); 223 | 224 | FD_ZERO(&rfds); 225 | FD_SET(fd, &rfds); 226 | 227 | ret = select(fd+1, &rfds, NULL, NULL, &tv); 228 | 229 | if (ret == -1 && errno != EINTR) 230 | warning("waiting on inotify queue failed"); 231 | else if (ret > 0 && !(ret == 1 && FD_ISSET(fd, &rfds))) { 232 | flog(LOG_WARNING, "unexpected fd while waiting on inotify queue"); 233 | ret = 0; 234 | } 235 | 236 | return ret > 0; 237 | } 238 | 239 | 240 | /* 241 | exec run_loop for all correct entries in (r)queue directory 242 | NOT thread-safe, since readdir_r is unreliable with filenames > NAME_MAX 243 | (e.g., NTFS + 255 unicode chars get truncated to 256 chars w/o terminating NUL) 244 | */ 245 | static void retry_dir(const char *qtype, const char *qpath, const char *looppath) { 246 | /* [offsetof(struct dirent, d_name) + fpathconf(fd, _PC_NAME_MAX) + 1] */ 247 | struct dirent *de; 248 | struct stat st; 249 | DIR *qdir; 250 | int fd, run; 251 | 252 | flog(LOG_DEBUG, "retrying %s directories", qtype); 253 | 254 | /* open directory (O_CLOEXEC is implied) */ 255 | if ((qdir = opendir(qpath))) { 256 | /* get corresponding file descriptor for stat */ 257 | if ((fd = dirfd(qdir)) != -1) { 258 | for (errno = 0; !stop_requested() && ((de = readdir(qdir))); ) { 259 | run = 0; 260 | 261 | /* some filesystems don't support d_type, need to stat entry */ 262 | if (de->d_type == DT_UNKNOWN && is_msgdir(de->d_name)) { 263 | if (!fstatat(fd, de->d_name, &st, AT_SYMLINK_NOFOLLOW)) 264 | run = S_ISDIR(st.st_mode); 265 | else 266 | warning("fstat failed"); 267 | } 268 | else 269 | run = (de->d_type == DT_DIR && is_msgdir(de->d_name)); 270 | 271 | if (run) 272 | run_loop(qtype, de->d_name, looppath); 273 | } 274 | 275 | if (errno && errno != EINTR) 276 | warning("reading directory failed"); 277 | } 278 | else 279 | warning("dirfd failed"); 280 | 281 | /* close directory */ 282 | if (closedir(qdir)) 283 | warning("could not close directory"); 284 | } 285 | else 286 | warning("could not open directory"); 287 | } 288 | 289 | 290 | int main() { 291 | /* using NAME_MAX prevents EINVAL on read() (twice for UTF-16 on NTFS) */ 292 | char buf[sizeof(struct inotify_event) + NAME_MAX*2 + 1]; 293 | char *crtpath, *qpath, *rqpath, *looppath, *lsthost, *lstport; 294 | int sz, offset, rereg, evqok, retryid; 295 | struct inotify_event *iev; 296 | double retrytmout, lastclock; 297 | 298 | 299 | /* init logging */ 300 | syslog_init(); 301 | 302 | 303 | /* extract environment */ 304 | crtpath = alloc_env(CABLE_CERTS, "/" CERTS_NAME); 305 | qpath = alloc_env(CABLE_QUEUES, "/" QUEUE_NAME); 306 | rqpath = alloc_env(CABLE_QUEUES, "/" RQUEUE_NAME); 307 | looppath = alloc_env(CABLE_HOME, "/" LOOP_NAME); 308 | lsthost = alloc_env(CABLE_HOST, ""); 309 | lstport = alloc_env(CABLE_PORT, ""); 310 | 311 | 312 | /* initialize rng */ 313 | if (!rand_init()) 314 | warning("failed to initialize RNG"); 315 | 316 | 317 | /* initialize process accounting */ 318 | if (!init_process_acc()) 319 | warning("failed to initialize process accounting"); 320 | 321 | 322 | /* initialize webserver */ 323 | if (!init_server(crtpath, qpath, rqpath, lsthost, lstport)) { 324 | flog(LOG_ERR, "failed to initialize webserver"); 325 | return EXIT_FAILURE; 326 | } 327 | 328 | 329 | /* try to reregister watches as long as no signal caught */ 330 | for (lastclock = getmontime(), retryid = 0; !stop_requested(); ) { 331 | /* support empty CABLE_NOLOOP when testing, to act as pure server */ 332 | #ifdef TESTING 333 | if (getenv("CABLE_NOLOOP")) { 334 | sleepsec(RETRY_TMOUT); 335 | continue; 336 | } 337 | #endif 338 | 339 | wait_reg_watches(qpath, rqpath); 340 | 341 | /* read events as long as no signal caught and no unmount / move_self / etc. events read */ 342 | for (rereg = evqok = 0; !stop_requested() && !rereg; ) { 343 | /* wait for an event, or timeout (later blocking read() results in error) */ 344 | retrytmout = RETRY_TMOUT + RETRY_TMOUT * (rand_shift() / 2); 345 | 346 | if (wait_read(inotfd, retrytmout - (getmontime() - lastclock))) { 347 | /* read events (non-blocking), taking care to handle interrupts due to signals */ 348 | if ((sz = read(inotfd, buf, sizeof(buf))) == -1 && errno != EINTR) { 349 | /* happens buffer is too small (e.g., NTFS + 255 unicode chars) */ 350 | warning("error while reading from inotify queue"); 351 | rereg = 1; 352 | } 353 | 354 | /* process all events in buffer, sz = -1 and 0 are automatically ignored */ 355 | for (offset = 0; offset < sz && !stop_requested() && !rereg; evqok = 1) { 356 | /* get handler to next event in read buffer, and update offset */ 357 | iev = (struct inotify_event*) (buf + offset); 358 | offset += sizeof(struct inotify_event) + iev->len; 359 | 360 | /* 361 | IN_IGNORED is triggered by watched directory removal / fs unmount 362 | IN_MOVE_SELF is only triggered by move of actual watched directory 363 | (i.e., not its parent) 364 | */ 365 | if ((iev->mask & (IN_IGNORED | IN_UNMOUNT | IN_Q_OVERFLOW | IN_MOVE_SELF))) 366 | rereg = 1; 367 | 368 | /* ignore non-subdirectory events, and events with incorrect name */ 369 | else if (iev->len > 0 && (iev->mask & IN_ISDIR) && is_msgdir(iev->name)) { 370 | assert(iev->wd == inotqwd || iev->wd == inotrqwd); 371 | if (iev->wd == inotqwd || iev->wd == inotrqwd) { 372 | /* stop can be indicated here (while waiting for less processes) */ 373 | const char *qtype = (iev->wd == inotqwd) ? QUEUE_NAME : RQUEUE_NAME; 374 | run_loop(qtype, iev->name, looppath); 375 | } 376 | else 377 | flog(LOG_WARNING, "unknown watch descriptor"); 378 | } 379 | } 380 | } 381 | 382 | /* 383 | if sufficient time passed since last retries, retry again 384 | */ 385 | if (!stop_requested() && getmontime() - lastclock >= retrytmout) { 386 | /* alternate between queue dirs to prevent lock starvation on self-send */ 387 | if ((retryid ^= 1)) 388 | retry_dir(QUEUE_NAME, qpath, looppath); 389 | else 390 | retry_dir(RQUEUE_NAME, rqpath, looppath); 391 | 392 | lastclock = getmontime(); 393 | 394 | /* inotify is apparently unreliable on fuse, so reregister when no events */ 395 | if (!evqok) 396 | rereg = 1; 397 | evqok = 0; 398 | } 399 | } 400 | } 401 | 402 | 403 | unreg_watches(); 404 | 405 | if (!shutdown_server()) 406 | flog(LOG_WARNING, "failed to shutdown webserver"); 407 | 408 | dealloc_env(lstport); 409 | dealloc_env(lsthost); 410 | dealloc_env(looppath); 411 | dealloc_env(rqpath); 412 | dealloc_env(qpath); 413 | dealloc_env(crtpath); 414 | 415 | flog(LOG_INFO, "exiting"); 416 | closelog(); 417 | 418 | return EXIT_SUCCESS; 419 | } 420 | -------------------------------------------------------------------------------- /src/daemon.h: -------------------------------------------------------------------------------- 1 | #ifndef DAEMON_H 2 | #define DAEMON_H 3 | 4 | /* common constants */ 5 | #define VERSION "LIBERTE CABLE 3.0" 6 | 7 | #define MSGID_LENGTH 40 8 | #define USERNAME_LENGTH 32 9 | 10 | #endif 11 | -------------------------------------------------------------------------------- /src/hex2base32.c: -------------------------------------------------------------------------------- 1 | /* 2 | http://tools.ietf.org/html/rfc3548 3 | http://en.wikipedia.org/wiki/Base32 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | int main(int argc, char *argv[]) { 11 | char *hex, *b32; 12 | int len, i, j, digit, sum; 13 | 14 | if (argc != 2) { 15 | printf("%s \n", argv[0]); 16 | return 1; 17 | } 18 | 19 | hex = argv[1]; 20 | len = strlen(hex); 21 | 22 | if (len == 0 || len % 5 != 0) { 23 | printf("Argument length must be a positive multiple of 5\n"); 24 | return 1; 25 | } 26 | 27 | b32 = (char*) malloc(len/5 * 4 + 1); 28 | if (!b32) { 29 | printf("Memory allocation error\n"); 30 | return 1; 31 | } 32 | b32[len/5 * 4] = '\0'; 33 | 34 | for (i = 0; i < len; ) { 35 | for (j = sum = 0; j < 5; ++i, ++j) { 36 | if (hex[i] >= '0' && hex[i] <= '9') 37 | digit = hex[i] - '0'; 38 | else if (hex[i] >= 'a' && hex[i] <= 'f') 39 | digit = hex[i] - 'a' + 10; 40 | else if (hex[i] >= 'A' && hex[i] <= 'F') 41 | digit = hex[i] - 'A' + 10; 42 | else { 43 | printf("Non-hexadecimal character encountered\n"); 44 | return 1; 45 | } 46 | 47 | sum = sum * 16 + digit; 48 | } 49 | 50 | for (j = 1; j <= 4; ++j) { 51 | digit = sum % 32; 52 | sum /= 32; 53 | 54 | if (digit < 26) 55 | digit += 'a'; 56 | else 57 | digit = digit - 26 + '2'; 58 | 59 | b32[i/5 * 4 - j] = digit; 60 | } 61 | } 62 | 63 | puts(b32); 64 | 65 | free(b32); 66 | return 0; 67 | } 68 | -------------------------------------------------------------------------------- /src/mhdrop.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | 17 | /* flock timeout */ 18 | #define LOCK_TMOUT 300 19 | 20 | #define NOT_NUM ULLONG_MAX 21 | #define NUM_LEN 20 22 | typedef unsigned long long num_t; 23 | 24 | 25 | static volatile int alrm = 0; 26 | 27 | 28 | /* logging init */ 29 | static void syslog_init() { 30 | openlog("mhdrop", LOG_PID, LOG_MAIL); 31 | } 32 | 33 | /* logging */ 34 | static void flog(int priority, const char *format, ...) { 35 | va_list ap; 36 | 37 | va_start(ap, format); 38 | 39 | #ifndef TESTING 40 | vsyslog(priority, format, ap); 41 | #else 42 | fprintf(stderr, "[%d] cable: ", priority); 43 | vfprintf(stderr, format, ap); 44 | fprintf(stderr, "\n"); 45 | #endif 46 | 47 | va_end(ap); 48 | } 49 | 50 | static void flogexit(int priority, const char *format, ...) { 51 | va_list ap; 52 | 53 | va_start(ap, format); 54 | 55 | #ifndef TESTING 56 | vsyslog(priority, format, ap); 57 | #else 58 | fprintf(stderr, "[%d] cable: ", priority); 59 | vfprintf(stderr, format, ap); 60 | fprintf(stderr, "\n"); 61 | #endif 62 | 63 | va_end(ap); 64 | 65 | exit(EXIT_FAILURE); 66 | } 67 | 68 | 69 | /* support for flock timeout */ 70 | static void alrm_handler(int signum) { 71 | if (signum == SIGALRM) 72 | alrm = 1; 73 | } 74 | 75 | 76 | /* fatal errors which shouldn't happen in a correct program */ 77 | static void error() { 78 | flogexit(LOG_ERR, "%m"); 79 | } 80 | 81 | 82 | /* set signal handlers */ 83 | static void set_signals() { 84 | struct sigaction sa; 85 | 86 | sa.sa_handler = alrm_handler; 87 | sa.sa_flags = 0; 88 | sa.sa_restorer = NULL; 89 | sigemptyset(&sa.sa_mask); 90 | 91 | if (sigaction(SIGALRM, &sa, NULL) == -1) 92 | error(); 93 | } 94 | 95 | 96 | /* 97 | convert file name to a number, if possible 98 | if not possible, NOT_NUM is returned 99 | */ 100 | static num_t getidx(const char *name) { 101 | char *end; 102 | size_t len, i; 103 | num_t num; 104 | 105 | len = strlen(name); 106 | 107 | /* check [1-9][0-9]* format */ 108 | if (!*name || name[0] == '0') 109 | return NOT_NUM; 110 | 111 | for (i = 0; i < len; ++i) 112 | if (!(name[i] >= '0' && name[i] <= '9')) 113 | return NOT_NUM; 114 | 115 | /* convert to number, checking overflow / would-be overflow */ 116 | errno = 0; 117 | num = strtoull(name, &end, 10); 118 | if (*end || errno || num == ULLONG_MAX) 119 | return NOT_NUM; 120 | 121 | return num; 122 | } 123 | 124 | 125 | /* 126 | locks are automatically released on program exit, 127 | so don't bother with unlocking on exceptions 128 | */ 129 | int main(int argc, char *argv[]) { 130 | const char *mhdir; 131 | struct dirent *de = NULL; 132 | DIR *dir; 133 | int dfd, i, spret; 134 | num_t maxidx = 0, curidx; 135 | char numname[NUM_LEN+1]; 136 | 137 | if (argc < 3) { 138 | fprintf(stderr, "%s ...\n", argv[0]); 139 | return 1; 140 | } 141 | 142 | mhdir = argv[1]; 143 | 144 | /* init logging */ 145 | syslog_init(); 146 | 147 | /* signals */ 148 | set_signals(); 149 | 150 | /* open directory */ 151 | if ((dir = opendir(mhdir)) == NULL) 152 | error(); 153 | 154 | /* get corresponding file descriptor for lock / access */ 155 | if ((dfd = dirfd(dir)) == -1) 156 | error(); 157 | 158 | /* lock directory with timeout */ 159 | alarm(LOCK_TMOUT); 160 | if (flock(dfd, LOCK_EX) == -1) { 161 | if (errno == EINTR && alrm) 162 | flogexit(LOG_ERR, "timed out while locking %s", mhdir); 163 | else 164 | error(); 165 | } 166 | alarm(0); 167 | 168 | /* find max entry, don't bother with file types */ 169 | for (errno = 0; (de = readdir(dir)) != NULL; ) 170 | if ((curidx = getidx(de->d_name)) != NOT_NUM && curidx > maxidx) 171 | maxidx = curidx; 172 | 173 | if (de == NULL && errno) 174 | error(); 175 | 176 | 177 | /* deliver messages */ 178 | for (i = 2, ++maxidx; i < argc; ++i, ++maxidx) { 179 | if (maxidx == NOT_NUM || maxidx == 0) 180 | flogexit(LOG_ERR, "indexes exhausted, %llu not legal", maxidx); 181 | 182 | /* 183 | convert to string, but also check back due to possible locale-related problems 184 | (unlikely if setlocale() is not explicitly invoked 185 | */ 186 | spret = snprintf(numname, sizeof(numname), "%llu", maxidx); 187 | if (spret < 0 || spret >= sizeof(numname) || getidx(numname) != maxidx) 188 | flogexit(LOG_ERR, "could not convert %llu to file name", maxidx); 189 | 190 | /* rename() may replace an externally created file, so use link/unlink */ 191 | if (linkat(AT_FDCWD, argv[i], dfd, numname, 0) == -1) 192 | error(); 193 | if (unlink(argv[i]) == -1) 194 | error(); 195 | 196 | flog(LOG_INFO, "delivered %s/%s", mhdir, numname); 197 | } 198 | 199 | 200 | /* unlock (explicitly) */ 201 | if (flock(dfd, LOCK_UN) == -1) 202 | error(); 203 | 204 | /* close directory */ 205 | if (closedir(dir) == -1) 206 | error(); 207 | 208 | closelog(); 209 | return 0; 210 | } 211 | -------------------------------------------------------------------------------- /src/process.c: -------------------------------------------------------------------------------- 1 | /* 2 | NOT thread-safe! 3 | */ 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "process.h" 14 | #include "util.h" 15 | 16 | 17 | /* fast shutdown indicator */ 18 | static volatile int stop; 19 | 20 | 21 | /* 22 | process counters (not used if initok is 0) 23 | pstarted: incremented in run_process() 24 | pfinished: incremented in chld_handler() 25 | */ 26 | static volatile long pstarted = 0, pfinished = 0; 27 | static int initok; 28 | 29 | 30 | int stop_requested() { 31 | return stop; 32 | } 33 | 34 | 35 | static void stop_handler(int signum) { 36 | assert(signum == SIGINT || signum == SIGTERM); 37 | if (signum == SIGINT || signum == SIGTERM) { 38 | if (!stop) { 39 | stop = 1; 40 | 41 | #ifndef TESTING 42 | /* kill pgroup; also sends signal to self, but stop=1 prevents recursion */ 43 | kill(0, SIGTERM); 44 | #endif 45 | } 46 | } 47 | } 48 | 49 | 50 | static void chld_handler(int signum) { 51 | pid_t pid; 52 | int status; 53 | 54 | assert(signum == SIGCHLD); 55 | if (signum == SIGCHLD) { 56 | /* 57 | multiple instances of pending signals are compressed, so 58 | handle all completed processes as soon as possible 59 | */ 60 | while ((pid = waitpid(0, &status, WNOHANG)) > 0) 61 | ++pfinished; 62 | 63 | /* -1/ECHILD is returned for no pending completed processes */ 64 | assert(pid != -1 || errno == ECHILD); 65 | } 66 | } 67 | 68 | 69 | int init_process_acc() { 70 | struct sigaction sa; 71 | memset(&sa, 0, sizeof(sa)); 72 | 73 | stop = 0; 74 | 75 | /* restart system calls interrupted by SIGCHLD */ 76 | sa.sa_handler = chld_handler; 77 | sa.sa_flags = SA_RESTART; 78 | 79 | /* SIGCHLD is automatically added to the mask */ 80 | initok = !sigemptyset(&sa.sa_mask) 81 | && !sigaction(SIGCHLD, &sa, NULL); 82 | 83 | sa.sa_handler = stop_handler; 84 | sa.sa_flags = 0; 85 | 86 | initok = initok 87 | && !sigaction(SIGINT, &sa, NULL) 88 | && !sigaction(SIGTERM, &sa, NULL); 89 | 90 | return initok; 91 | } 92 | 93 | 94 | int run_process(long maxproc, double waitsec, const char *const argv[]) { 95 | int res = 0; 96 | long pcount; 97 | pid_t pid; 98 | 99 | /* 100 | wait if too many processes have been launched 101 | SIGCHLD from terminated processes also interrupts sleep 102 | */ 103 | while (initok && !stop_requested() && (pcount = pstarted - pfinished) >= maxproc) { 104 | flog(LOG_NOTICE, "too many processes (%ld), waiting...", pcount); 105 | sleepsec(waitsec); 106 | } 107 | 108 | if (!stop_requested()) { 109 | if ((pid = fork()) == -1) 110 | warning("fork failed"); 111 | else if (pid == 0) { 112 | /* modifiable strings signature seems to be historic */ 113 | execvp(argv[0], (char *const *) argv); 114 | 115 | /* exits just the fork */ 116 | error("loop execution failed"); 117 | } 118 | else { 119 | ++pstarted; 120 | res = 1; 121 | } 122 | } 123 | 124 | return res; 125 | } 126 | -------------------------------------------------------------------------------- /src/process.h: -------------------------------------------------------------------------------- 1 | #ifndef PROCESS_H 2 | #define PROCESS_H 3 | 4 | int init_process_acc(); 5 | int run_process(long maxproc, double waitsec, const char *const argv[]); 6 | 7 | int stop_requested(); 8 | 9 | #endif 10 | -------------------------------------------------------------------------------- /src/server.c: -------------------------------------------------------------------------------- 1 | /* 2 | + / common URL prefix: CABLE_CERTS/certs/username 3 | + /certs/{ca,verify}.pem serve CABLE_CERTS/certs/{ca,verify}.pem 4 | + /queue/ serve CABLE_QUEUES/queue//message.enc 5 | + /queue/.key serve CABLE_QUEUES/queue//speer.sig 6 | + /rqueue/.key serve CABLE_QUEUES/rqueue//rpeer.sig 7 | + /request/... invoke service(...), and return answer 8 | */ 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #define MHD_PLATFORM_H 18 | #include 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | #ifndef EAI_ADDRFAMILY 28 | #define EAI_ADDRFAMILY -9 29 | #endif 30 | 31 | #include "server.h" 32 | #include "daemon.h" 33 | #include "service.h" 34 | #include "util.h" 35 | 36 | 37 | /* retry and limit strategies */ 38 | #ifndef TESTING 39 | #define WAIT_CONN 100 40 | #define MAX_THREAD 4 41 | #else 42 | #define WAIT_CONN 10 43 | #define MAX_THREAD 2 44 | #endif 45 | 46 | /* path and url suffixes */ 47 | #define USERNAME_SFX "username" 48 | #define CA_SFX "ca.pem" 49 | #define VERIFY_SFX "verify.pem" 50 | #define MESSAGE_SFX "message.enc" 51 | #define SPEER_SFX "speer.sig" 52 | #define RPEER_SFX "rpeer.sig" 53 | #define KEY_SFX ".key" 54 | 55 | /* url prefixes */ 56 | #define CERTS_PFX "/certs/" 57 | #define QUEUE_PFX "/queue/" 58 | #define RQUEUE_PFX "/rqueue/" 59 | #define REQUEST_PFX "/request/" 60 | 61 | /* service responses */ 62 | #define SVC_RESP_OK VERSION "\n" 63 | #define SVC_RESP_ERR VERSION ": ERROR\n" 64 | 65 | 66 | /* read-only values after server startup */ 67 | static struct MHD_Daemon *mhd_daemon; 68 | static struct MHD_Response *mhd_empty, *mhd_svc_ok, *mhd_svc_err; 69 | static const char *crt_path, *cq_path, *crq_path; 70 | static char username[USERNAME_LENGTH+2]; 71 | 72 | 73 | static int advance_pfx(const char **url, const char *pfx) { 74 | size_t len = strlen(pfx); 75 | int ret = 0; 76 | 77 | if (!strncmp(*url, pfx, len)) { 78 | *url += len; 79 | ret = 1; 80 | } 81 | 82 | return ret; 83 | } 84 | 85 | 86 | /* 87 | dir + [ / subdir ] + sfx 88 | */ 89 | static int queue_fd(struct MHD_Connection *connection, 90 | const char *dir, const char *subdir, const char *sfx) { 91 | char path[strlen(dir) + (subdir ? strlen(subdir) + 1 : 0) + strlen(sfx) + 1]; 92 | struct MHD_Response *resp; 93 | struct stat st; 94 | int ret, fd; 95 | 96 | /* construct full path */ 97 | strcpy(path, dir); 98 | if (subdir) { 99 | strcat(path, "/"); 100 | strcat(path, subdir); 101 | } 102 | strcat(path, sfx); 103 | 104 | if ((fd = open(path, O_RDONLY | O_CLOEXEC)) != -1) { 105 | if (!fstat(fd, &st) && ((resp = MHD_create_response_from_fd(st.st_size, fd)))) { 106 | ret = MHD_queue_response(connection, MHD_HTTP_OK, resp); 107 | MHD_destroy_response(resp); 108 | } 109 | else { 110 | close(fd); 111 | ret = MHD_queue_response(connection, MHD_HTTP_INTERNAL_SERVER_ERROR, mhd_empty); 112 | } 113 | } 114 | else 115 | ret = MHD_queue_response(connection, MHD_HTTP_NOT_FOUND, mhd_empty); 116 | 117 | return ret; 118 | } 119 | 120 | 121 | static int handle_connection(void *cls, struct MHD_Connection *connection, 122 | const char *url, const char *method, const char *version, 123 | const char *upload_data, size_t *upload_data_size, 124 | void **con_cls) { 125 | enum SVC_Status svc_status; 126 | char msgid[MSGID_LENGTH + sizeof(KEY_SFX)]; 127 | int ret; 128 | 129 | /* support GET only, close connection otherwise */ 130 | if ((strcmp(method, MHD_HTTP_METHOD_GET) && strcmp(method, MHD_HTTP_METHOD_HEAD)) 131 | || *upload_data_size) 132 | ret = MHD_queue_response(connection, MHD_HTTP_METHOD_NOT_ALLOWED, mhd_empty); 133 | 134 | /* do not queue response on first call (enabled pipelining) */ 135 | else if (!*con_cls) { 136 | *con_cls = ""; 137 | ret = MHD_YES; 138 | } 139 | 140 | /* check / prefix, close connection if no match */ 141 | else if (!(*(url++) == '/' && advance_pfx(&url, username))) 142 | ret = MHD_queue_response(connection, MHD_HTTP_FORBIDDEN, mhd_empty); 143 | 144 | /* handle username-authenticated queries */ 145 | else { 146 | /* serve /certs/ files */ 147 | if ( !strcmp(url, CERTS_PFX CA_SFX)) 148 | ret = queue_fd(connection, crt_path, NULL, "/" CA_SFX); 149 | else if (!strcmp(url, CERTS_PFX VERIFY_SFX)) 150 | ret = queue_fd(connection, crt_path, NULL, "/" VERIFY_SFX); 151 | 152 | /* serve /queue/{,.key} and /rqueue/.key */ 153 | else if (advance_pfx(&url, QUEUE_PFX)) { 154 | strncpy(msgid, url, sizeof(msgid)); 155 | 156 | if (!msgid[MSGID_LENGTH] && vfyhex(MSGID_LENGTH, msgid)) 157 | ret = queue_fd(connection, cq_path, msgid, "/" MESSAGE_SFX); 158 | else if (!strncmp(msgid + MSGID_LENGTH, KEY_SFX, sizeof(KEY_SFX)) 159 | && (msgid[MSGID_LENGTH] = '\0', vfyhex(MSGID_LENGTH, msgid))) 160 | ret = queue_fd(connection, cq_path, msgid, "/" SPEER_SFX); 161 | else 162 | ret = MHD_queue_response(connection, MHD_HTTP_FORBIDDEN, mhd_empty); 163 | } 164 | else if (advance_pfx(&url, RQUEUE_PFX)) { 165 | strncpy(msgid, url, sizeof(msgid)); 166 | 167 | if (!strncmp(msgid + MSGID_LENGTH, KEY_SFX, sizeof(KEY_SFX)) 168 | && (msgid[MSGID_LENGTH] = '\0', vfyhex(MSGID_LENGTH, msgid))) 169 | ret = queue_fd(connection, crq_path, msgid, "/" RPEER_SFX); 170 | else 171 | ret = MHD_queue_response(connection, MHD_HTTP_FORBIDDEN, mhd_empty); 172 | } 173 | 174 | /* handle /request/ interface */ 175 | else if (advance_pfx(&url, REQUEST_PFX)) { 176 | svc_status = handle_request(url, cq_path, crq_path); 177 | 178 | switch (svc_status) { 179 | case SVC_OK: 180 | ret = MHD_queue_response(connection, MHD_HTTP_OK, mhd_svc_ok); 181 | break; 182 | case SVC_ERR: 183 | ret = MHD_queue_response(connection, MHD_HTTP_INTERNAL_SERVER_ERROR, mhd_svc_err); 184 | break; 185 | case SVC_BADFMT: 186 | ret = MHD_queue_response(connection, MHD_HTTP_BAD_REQUEST, mhd_svc_err); 187 | break; 188 | default: 189 | ret = MHD_NO; 190 | } 191 | } 192 | else 193 | ret = MHD_queue_response(connection, MHD_HTTP_FORBIDDEN, mhd_empty); 194 | } 195 | 196 | return ret; 197 | } 198 | 199 | 200 | static int read_username(const char *certs) { 201 | int res = 0, len; 202 | FILE *file; 203 | char path[strlen(certs) + sizeof("/" USERNAME_SFX)]; 204 | 205 | strcpy(path, certs); 206 | strcat(path, "/" USERNAME_SFX); 207 | 208 | if ((file = fopen(path, "re"))) { 209 | if(fgets(username, sizeof(username), file) && fgetc(file) == EOF) { 210 | len = strlen(username); 211 | if (username[len-1] == '\n') 212 | username[len-1] = '\0'; 213 | 214 | if (vfybase32(USERNAME_LENGTH, username)) 215 | res = 1; 216 | } 217 | 218 | if (fclose(file)) 219 | res = 0; 220 | } 221 | 222 | return res; 223 | } 224 | 225 | 226 | static int ignore_sigpipe() { 227 | struct sigaction sa; 228 | memset(&sa, 0, sizeof(sa)); 229 | 230 | /* restart system calls interrupted by SIGPIPE (if not using SIG_IGN) */ 231 | sa.sa_handler = SIG_IGN; 232 | sa.sa_flags = SA_RESTART; 233 | 234 | /* SIGPIPE is automatically added to the mask */ 235 | return !sigemptyset(&sa.sa_mask) 236 | && !sigaction(SIGPIPE, &sa, NULL); 237 | } 238 | 239 | 240 | int init_server(const char *certs, const char *qpath, const char *rqpath, 241 | const char *host, const char *port) { 242 | #ifdef TESTING 243 | const enum MHD_FLAG extra_flags = MHD_USE_DEBUG; 244 | #else 245 | const enum MHD_FLAG extra_flags = 0; 246 | #endif 247 | struct addrinfo addr_hints, *address; 248 | int addr_res; 249 | 250 | 251 | /* save paths */ 252 | crt_path = certs; 253 | cq_path = qpath; 254 | crq_path = rqpath; 255 | 256 | 257 | /* ignore SIGPIPE, as recommended by libmicrohttpd */ 258 | if (!ignore_sigpipe()) 259 | warning("failed to ignore PIPE signal"); 260 | 261 | 262 | if (!read_username(certs)) { 263 | flog(LOG_ERR, "could not read %s/username", certs); 264 | return 0; 265 | } 266 | 267 | 268 | /* create immutable responses */ 269 | if ( !(mhd_empty = MHD_create_response_from_buffer(0, NULL, MHD_RESPMEM_PERSISTENT)) 270 | || !(mhd_svc_ok = MHD_create_response_from_buffer(sizeof(SVC_RESP_OK)-1, SVC_RESP_OK, MHD_RESPMEM_PERSISTENT)) 271 | || !(mhd_svc_err = MHD_create_response_from_buffer(sizeof(SVC_RESP_ERR)-1, SVC_RESP_ERR, MHD_RESPMEM_PERSISTENT)) 272 | || MHD_NO == MHD_add_response_header(mhd_svc_ok, MHD_HTTP_HEADER_CONTENT_TYPE, "text/plain") 273 | || MHD_NO == MHD_add_response_header(mhd_svc_ok, MHD_HTTP_HEADER_CACHE_CONTROL, "no-cache") 274 | || MHD_NO == MHD_add_response_header(mhd_svc_err, MHD_HTTP_HEADER_CONTENT_TYPE, "text/plain")) 275 | return 0; 276 | 277 | 278 | /* translate host address */ 279 | memset(&addr_hints, 0, sizeof(addr_hints)); 280 | addr_hints.ai_family = AF_UNSPEC /* AF_INET causes EAI_NONAME when network is down */; 281 | addr_hints.ai_socktype = SOCK_STREAM; 282 | addr_hints.ai_protocol = IPPROTO_TCP; 283 | addr_hints.ai_flags = AI_V4MAPPED | AI_ADDRCONFIG | AI_PASSIVE; 284 | 285 | if (((addr_res = getaddrinfo((*host ? host : NULL), port, &addr_hints, &address))) 286 | || (address->ai_family != AF_INET && ((addr_res = EAI_ADDRFAMILY)))) { 287 | flog(LOG_ERR, "%s", gai_strerror(addr_res)); 288 | return 0; 289 | } 290 | 291 | 292 | if (!(mhd_daemon = MHD_start_daemon( 293 | MHD_USE_SELECT_INTERNALLY | MHD_USE_PEDANTIC_CHECKS | MHD_SUPPRESS_DATE_NO_CLOCK | extra_flags, 294 | /* port, ignored if sock_addr is specified, but must be non-0 */ 295 | -1, 296 | /* MHD_AcceptPolicyCallback */ 297 | NULL, NULL, 298 | /* MHD_AccessHandlerCallback */ 299 | handle_connection, NULL, 300 | /* options section */ 301 | MHD_OPTION_CONNECTION_LIMIT, (unsigned) WAIT_CONN, 302 | MHD_OPTION_THREAD_POOL_SIZE, (unsigned) MAX_THREAD, 303 | MHD_OPTION_SOCK_ADDR, address->ai_addr, 304 | MHD_OPTION_END))) 305 | return 0; 306 | 307 | 308 | freeaddrinfo(address); 309 | 310 | return 1; 311 | } 312 | 313 | int shutdown_server() { 314 | MHD_stop_daemon(mhd_daemon); 315 | 316 | MHD_destroy_response(mhd_svc_err); 317 | MHD_destroy_response(mhd_svc_ok); 318 | MHD_destroy_response(mhd_empty); 319 | 320 | return 1; 321 | } 322 | -------------------------------------------------------------------------------- /src/server.h: -------------------------------------------------------------------------------- 1 | #ifndef SERVER_H 2 | #define SERVER_H 3 | 4 | int init_server(const char *certs, const char *qpath, const char *rqpath, const char *host, const char *port); 5 | int shutdown_server(); 6 | 7 | #endif 8 | -------------------------------------------------------------------------------- /src/service.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "service.h" 11 | #include "daemon.h" 12 | #include "util.h" 13 | 14 | 15 | #define MAX_REQUEST_LENGTH 255 16 | 17 | #define TOR_HOSTNAME_LENGTH 16 18 | #define I2P_HOSTNAME_LENGTH 52 19 | #define MAC_LENGTH 128 20 | 21 | #define DCREAT_MODE (S_IRWXU | S_IRWXG | S_IRWXO) 22 | #define FCREAT_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH) 23 | 24 | 25 | /* lowercase hostnames: recognizes .onion and .b32.i2p addresses */ 26 | static int vfyhost(char *s) { 27 | int result = 0; 28 | char *dot = strchr(s, '.'); 29 | 30 | if (dot) { 31 | *dot = '\0'; 32 | 33 | /* Tor .onion hostnames */ 34 | if (!strcmp("onion", dot+1)) 35 | result = vfybase32(TOR_HOSTNAME_LENGTH, s); 36 | 37 | /* I2P .b32.i2p hostnames */ 38 | else if (!strcmp("b32.i2p", dot+1)) 39 | result = vfybase32(I2P_HOSTNAME_LENGTH, s); 40 | 41 | *dot = '.'; 42 | } 43 | 44 | return result; 45 | } 46 | 47 | 48 | static int write_line(int dir, const char *path, const char *s) { 49 | int res = 0, fd; 50 | FILE *file; 51 | 52 | if ((fd = openat(dir, path, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, FCREAT_MODE)) != -1) { 53 | if ((file = fdopen(fd, "w"))) { 54 | if (s) { 55 | if (fputs(s, file) >= 0 && fputc('\n', file) == '\n') 56 | res = 1; 57 | } 58 | else 59 | res = 1; 60 | 61 | if (fclose(file)) 62 | res = 0; 63 | } 64 | else 65 | close(fd); 66 | } 67 | 68 | return res; 69 | } 70 | 71 | 72 | static int read_line(int dir, const char *path, char *s, int sz) { 73 | int res = 0, fd; 74 | FILE *file; 75 | 76 | if ((fd = openat(dir, path, O_RDONLY | O_CLOEXEC)) != -1) { 77 | if ((file = fdopen(fd, "r"))) { 78 | if(fgets(s, sz, file) && fgetc(file) == EOF) { 79 | sz = strlen(s); 80 | if (s[sz-1] == '\n') 81 | s[sz-1] = '\0'; 82 | 83 | res = 1; 84 | } 85 | 86 | if (fclose(file)) 87 | res = 0; 88 | } 89 | else 90 | close(fd); 91 | } 92 | 93 | return res; 94 | } 95 | 96 | 97 | static int check_file(int dir, const char *path) { 98 | return !faccessat(dir, path, F_OK, 0); 99 | } 100 | 101 | 102 | static int create_file(int dir, const char *path) { 103 | return check_file(dir, path) || write_line(dir, path, NULL); 104 | } 105 | 106 | 107 | /* attemts non-blocking lock (note: errno == EWOULDBLOCK) */ 108 | static int try_lock(int fd) { 109 | return !flock(fd, LOCK_EX | LOCK_NB); 110 | } 111 | 112 | 113 | static int handle_msg(const char *msgid, const char *hostname, 114 | const char *username, int cqdir) { 115 | int res = 0, msgdir; 116 | char msgidnew[MSGID_LENGTH+4+1]; 117 | 118 | /* checkno /cables/rqueue/ (ok and skip if exists) */ 119 | if (check_file(cqdir, msgid)) 120 | res = 1; 121 | 122 | else if (errno == ENOENT) { 123 | /* temp base: .../cables/rqueue/.new */ 124 | strncpy(msgidnew, msgid, MSGID_LENGTH); 125 | strcpy(msgidnew + MSGID_LENGTH, ".new"); 126 | 127 | /* create directory (ok if exists) */ 128 | if (!mkdirat(cqdir, msgidnew, DCREAT_MODE) || errno == EEXIST) { 129 | if ((msgdir = openat(cqdir, msgidnew, O_RDONLY | O_CLOEXEC)) != -1) { 130 | res = 131 | /* lock temp base */ 132 | try_lock(msgdir) 133 | /* write hostname */ 134 | && write_line(msgdir, "hostname", hostname) 135 | /* write username */ 136 | && write_line(msgdir, "username", username) 137 | /* create peer.req */ 138 | && create_file(msgdir, "peer.req") 139 | /* rename .../cables/rqueue/.new -> */ 140 | && !renameat(cqdir, msgidnew, cqdir, msgid); 141 | 142 | /* close base (and unlock if locked) */ 143 | if (close(msgdir)) 144 | res = 0; 145 | } 146 | } 147 | } 148 | 149 | return res; 150 | } 151 | 152 | 153 | static int handle_snd(const char *msgid, const char *mac, int cqdir) { 154 | int res = 0, msgdir; 155 | 156 | /* base: .../cables/rqueue/ */ 157 | if ((msgdir = openat(cqdir, msgid, O_RDONLY | O_CLOEXEC)) != -1) { 158 | if (/* lock base */ 159 | try_lock(msgdir) 160 | /* check peer.ok */ 161 | && check_file(msgdir, "peer.ok") 162 | /* write send.mac (skip if exists) */ 163 | && (check_file(msgdir, "send.mac") 164 | || (errno == ENOENT && write_line(msgdir, "send.mac", mac)))) { 165 | 166 | /* create recv.req (atomic, ok if exists) */ 167 | if (linkat(msgdir, "peer.ok", msgdir, "recv.req", 0)) 168 | res = (errno == EEXIST); 169 | else 170 | res = 171 | /* unlock base (touch triggers loop's lock) */ 172 | !flock(msgdir, LOCK_UN) 173 | /* touch /cables/rqueue// (if recv.req didn't exist) */ 174 | /* euid owns msgdir, so O_RDWR is not needed (NOTE: unless overlayfs) */ 175 | && !futimens(msgdir, NULL); 176 | } 177 | 178 | /* close base (and unlock if locked) */ 179 | if (close(msgdir)) 180 | res = 0; 181 | } 182 | 183 | return res; 184 | } 185 | 186 | 187 | static int handle_rcp(const char *msgid, const char *mac, int cqdir) { 188 | int res = 0, msgdir; 189 | char exmac[MAC_LENGTH+2]; 190 | 191 | /* base: .../cables/queue/ */ 192 | if ((msgdir = openat(cqdir, msgid, O_RDONLY | O_CLOEXEC)) != -1) { 193 | if (/* lock base */ 194 | try_lock(msgdir) 195 | /* check send.ok */ 196 | && check_file(msgdir, "send.ok") 197 | /* read recv.mac */ 198 | && read_line(msgdir, "recv.mac", exmac, sizeof(exmac)) 199 | /* compare <-> recv.mac */ 200 | && !strcmp(mac, exmac)) { 201 | 202 | /* create ack.req (atomic, ok if exists) */ 203 | if (linkat(msgdir, "send.ok", msgdir, "ack.req", 0)) 204 | res = (errno == EEXIST); 205 | else 206 | res = 207 | /* unlock base (touch triggers loop's lock) */ 208 | !flock(msgdir, LOCK_UN) 209 | /* touch /cables/queue// (if ack.req didn't exist) */ 210 | /* euid owns msgdir, so O_RDWR is not needed (NOTE: unless overlayfs) */ 211 | && !futimens(msgdir, NULL); 212 | } 213 | 214 | /* close base (and unlock if locked) */ 215 | if (close(msgdir)) 216 | res = 0; 217 | } 218 | 219 | return res; 220 | } 221 | 222 | 223 | static int handle_ack(const char *msgid, const char *mac, int cqdir) { 224 | int res = 0, msgdir; 225 | char msgiddel[MSGID_LENGTH+4+1], exmac[MAC_LENGTH+2]; 226 | 227 | /* base: .../cables/rqueue/ */ 228 | if ((msgdir = openat(cqdir, msgid, O_RDONLY | O_CLOEXEC)) != -1) { 229 | strncpy(msgiddel, msgid, MSGID_LENGTH); 230 | strcpy(msgiddel + MSGID_LENGTH, ".del"); 231 | 232 | res = 233 | /* lock base */ 234 | try_lock(msgdir) 235 | /* check recv.ok */ 236 | && check_file(msgdir, "recv.ok") 237 | /* read ack.mac */ 238 | && read_line(msgdir, "ack.mac", exmac, sizeof(exmac)) 239 | /* compare <-> ack.mac */ 240 | && !strcmp(mac, exmac) 241 | /* rename .../cables/rqueue/ -> .del */ 242 | && !renameat(cqdir, msgid, cqdir, msgiddel); 243 | 244 | /* close base (and unlock if locked) */ 245 | if (close(msgdir)) 246 | res = 0; 247 | } 248 | 249 | return res; 250 | } 251 | 252 | 253 | /* 254 | returns memory-persistent response (including trailing newline) 255 | thread-safe 256 | does not leak memory / file descriptors 257 | */ 258 | enum SVC_Status handle_request(const char *request, const char *queues, const char *rqueues) { 259 | enum SVC_Status status = SVC_BADFMT; 260 | char buf[MAX_REQUEST_LENGTH+1], *saveptr, *cmd, *msgid, *arg1, *arg2; 261 | int cqdir; 262 | size_t reqlen; 263 | 264 | 265 | /* Copy request to modifiable buffer, check for length and bad delimiters */ 266 | reqlen = strlen(request); 267 | if (reqlen < sizeof(buf) && reqlen > 0 268 | && !strstr(request, "//") 269 | && request[0] != '/' && request[reqlen-1] != '/') { 270 | strcpy(buf, request); 271 | 272 | /* Tokenize the request */ 273 | cmd = strtok_r(buf, "/", &saveptr); 274 | msgid = strtok_r(NULL, "/", &saveptr); 275 | arg1 = strtok_r(NULL, "/", &saveptr); 276 | arg2 = strtok_r(NULL, "/", &saveptr); 277 | 278 | if (cmd && !strtok_r(NULL, "/", &saveptr)) { 279 | /* 280 | ver 281 | msg/// 282 | snd// 283 | rcp// 284 | ack// 285 | 286 | msgid: MSGID_LENGTH lowercase xdigits 287 | mac: MAC_LENGTH lowercase xdigits 288 | hostname: TOR_HOSTNAME_LENGTH lowercase base-32 chars + ".onion" 289 | I2P_HOSTNAME_LENGTH lowercase base-32 chars + ".b32.i2p" 290 | username: USERNAME_LENGTH lowercase base-32 chars 291 | */ 292 | if (!strcmp("ver", cmd)) { 293 | if (!msgid) 294 | status = SVC_OK; 295 | } 296 | else if (!strcmp("msg", cmd)) { 297 | if (arg2 298 | && vfyhex(MSGID_LENGTH, msgid) 299 | && vfyhost(arg1) 300 | && vfybase32(USERNAME_LENGTH, arg2)) { 301 | 302 | status = SVC_ERR; 303 | 304 | if ((cqdir = open(rqueues, O_RDONLY | O_CLOEXEC)) != -1) { 305 | if (handle_msg(msgid, arg1, arg2, cqdir)) 306 | status = SVC_OK; 307 | 308 | if (close(cqdir)) 309 | status = SVC_ERR; 310 | } 311 | } 312 | } 313 | else if (!strcmp("snd", cmd)) { 314 | if (arg1 && !arg2 315 | && vfyhex(MSGID_LENGTH, msgid) 316 | && vfyhex(MAC_LENGTH, arg1)) { 317 | 318 | status = SVC_ERR; 319 | 320 | if ((cqdir = open(rqueues, O_RDONLY | O_CLOEXEC)) != -1) { 321 | if (handle_snd(msgid, arg1, cqdir)) 322 | status = SVC_OK; 323 | 324 | if (close(cqdir)) 325 | status = SVC_ERR; 326 | } 327 | } 328 | } 329 | else if (!strcmp("rcp", cmd)) { 330 | if (arg1 && !arg2 331 | && vfyhex(MSGID_LENGTH, msgid) 332 | && vfyhex(MAC_LENGTH, arg1)) { 333 | 334 | status = SVC_ERR; 335 | 336 | if ((cqdir = open(queues, O_RDONLY | O_CLOEXEC)) != -1) { 337 | if (handle_rcp(msgid, arg1, cqdir)) 338 | status = SVC_OK; 339 | 340 | if (close(cqdir)) 341 | status = SVC_ERR; 342 | } 343 | } 344 | } 345 | else if (!strcmp("ack", cmd)) { 346 | if (arg1 && !arg2 347 | && vfyhex(MSGID_LENGTH, msgid) 348 | && vfyhex(MAC_LENGTH, arg1)) { 349 | 350 | status = SVC_ERR; 351 | 352 | if ((cqdir = open(rqueues, O_RDONLY | O_CLOEXEC)) != -1) { 353 | if (handle_ack(msgid, arg1, cqdir)) 354 | status = SVC_OK; 355 | 356 | if (close(cqdir)) 357 | status = SVC_ERR; 358 | } 359 | } 360 | } 361 | } 362 | } 363 | 364 | return status; 365 | } 366 | -------------------------------------------------------------------------------- /src/service.h: -------------------------------------------------------------------------------- 1 | #ifndef SERVICE_H 2 | #define SERVICE_H 3 | 4 | enum SVC_Status { 5 | SVC_BADFMT = -1, 6 | SVC_ERR = 0, 7 | SVC_OK = 1 8 | }; 9 | 10 | enum SVC_Status handle_request(const char *request, const char *queues, const char *rqueues); 11 | 12 | #endif 13 | -------------------------------------------------------------------------------- /src/su/dee/i2p/EepPriv.java: -------------------------------------------------------------------------------- 1 | package su.dee.i2p; 2 | 3 | import java.io.BufferedWriter; 4 | import java.io.File; 5 | import java.io.FileWriter; 6 | import java.io.PrintWriter; 7 | 8 | import net.i2p.data.Base32; 9 | import net.i2p.data.Destination; 10 | import net.i2p.data.PrivateKeyFile; 11 | 12 | /** 13 | * @brief Generates eepPriv.dat and its corresponding .b32.i2p / b64 hostnames. 14 | */ 15 | public class EepPriv { 16 | private static final String PKF_NAME = "eepPriv.dat"; 17 | private static final String PKF_B32 = "hostname"; 18 | private static final String PKF_B64 = "hostname.b64"; 19 | 20 | public static void main(String[] args) { 21 | if (args.length != 1) { 22 | System.out.println("Format: java " + EepPriv.class.getName() + " "); 23 | System.exit(1); 24 | } 25 | 26 | try { 27 | File dir = new File(args[0]); 28 | File eepFile = new File(dir, PKF_NAME); 29 | File b32File = new File(dir, PKF_B32); 30 | File b64File = new File(dir, PKF_B64); 31 | 32 | PrivateKeyFile pkf = new PrivateKeyFile(eepFile); 33 | Destination dest = pkf.createIfAbsent(); 34 | 35 | // hex2base32 $(head -c 387 eepPriv.dat | sha256sum | sed 's/[^[:xdigit:]].*/0/') 36 | // head -c 387 eepPriv.dat | mimencode | tr -d '\n' | tr +/ -~; echo 37 | String b32 = Base32.encode(dest.calculateHash().getData()) + ".b32.i2p"; 38 | String b64 = dest.toBase64(); 39 | 40 | PrintWriter b32out = new PrintWriter(new BufferedWriter(new FileWriter(b32File))); 41 | b32out.println(b32); 42 | b32out.close(); 43 | 44 | PrintWriter b64out = new PrintWriter(new BufferedWriter(new FileWriter(b64File))); 45 | b64out.println(b64); 46 | b64out.close(); 47 | } catch (Exception e) { 48 | e.printStackTrace(System.err); 49 | System.exit(1); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/util.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #ifdef TESTING 8 | #include 9 | #endif 10 | 11 | #include "util.h" 12 | 13 | 14 | /* logging init */ 15 | void syslog_init() { 16 | openlog("cable", LOG_PID, LOG_MAIL); 17 | } 18 | 19 | /* logging */ 20 | void flog(int priority, const char *format, ...) { 21 | va_list ap; 22 | va_start(ap, format); 23 | 24 | #ifndef TESTING 25 | vsyslog(priority, format, ap); 26 | #else 27 | fprintf(stderr, "[%d] cable: ", priority); 28 | vfprintf(stderr, format, ap); 29 | fprintf(stderr, "\n"); 30 | #endif 31 | 32 | va_end(ap); 33 | } 34 | 35 | /* non-fatal error */ 36 | void warning(const char *prefix) { 37 | flog(LOG_WARNING, "%s: %m", prefix); 38 | } 39 | 40 | /* fatal errors which shouldn't happen in a correct program */ 41 | void error(const char *prefix) { 42 | flog(LOG_CRIT, "%s: %m", prefix); 43 | exit(EXIT_FAILURE); 44 | } 45 | 46 | 47 | /* lowercase hexadecimal (0-9, a-f) */ 48 | int vfyhex(int sz, const char *s) { 49 | if (strlen(s) != sz) 50 | return 0; 51 | 52 | for (; *s; ++s) 53 | if (!((*s >= '0' && *s <= '9') || (*s >= 'a' && *s <= 'f'))) 54 | return 0; 55 | 56 | return 1; 57 | } 58 | 59 | 60 | /* lowercase Base-32 encoding (a-z, 2-7) */ 61 | int vfybase32(int sz, const char *s) { 62 | if (strlen(s) != sz) 63 | return 0; 64 | 65 | for (; *s; ++s) 66 | if (!((*s >= 'a' && *s <= 'z') || (*s >= '2' && *s <= '7'))) 67 | return 0; 68 | 69 | return 1; 70 | } 71 | 72 | 73 | /* allocate buffer for environment variable + suffix */ 74 | char* alloc_env(const char *var, const char *suffix) { 75 | const char *value; 76 | char *buf; 77 | size_t varlen; 78 | 79 | if (!((value = getenv(var)))) { 80 | flog(LOG_ERR, "environment variable %s is not set", var); 81 | exit(EXIT_FAILURE); 82 | } 83 | 84 | varlen = strlen(value); 85 | if (!((buf = (char*) malloc(varlen + strlen(suffix) + 1)))) 86 | error("malloc failed"); 87 | 88 | strncpy(buf, value, varlen); 89 | strcpy(buf + varlen, suffix); 90 | 91 | return buf; 92 | } 93 | 94 | void dealloc_env(char *env) { 95 | free(env); 96 | } 97 | 98 | 99 | /* initialize rng */ 100 | int rand_init() { 101 | struct timespec tp; 102 | 103 | return !clock_gettime(CLOCK_MONOTONIC, &tp) 104 | && (srandom(((unsigned) tp.tv_sec << 29) ^ (unsigned) tp.tv_nsec), 1); 105 | } 106 | 107 | 108 | /* uniformly distributed value in [-1, 1] */ 109 | double rand_shift() { 110 | return random() / (RAND_MAX / 2.0) - 1; 111 | } 112 | 113 | 114 | /* get strictly monotonic time (unaffected by ntp/htp) in seconds */ 115 | double getmontime() { 116 | struct timespec tp; 117 | 118 | if (clock_gettime(CLOCK_MONOTONIC, &tp)) 119 | error("failed to read monotonic clock"); 120 | 121 | return tp.tv_sec + tp.tv_nsec / 1e9; 122 | } 123 | 124 | /* 125 | sleep given number of seconds without interferences with SIGALRM 126 | do not complete interrupted sleeps, to facilitate fast process shutdown 127 | */ 128 | void sleepsec(double sec) { 129 | struct timespec req; 130 | 131 | /* support negative arguments */ 132 | if (sec > 0) { 133 | req.tv_sec = (time_t) sec; 134 | req.tv_nsec = (long) ((sec - req.tv_sec) * 1e9); 135 | 136 | if (nanosleep(&req, NULL) && errno != EINTR) 137 | warning("sleep failed"); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/util.h: -------------------------------------------------------------------------------- 1 | #ifndef UTIL_H 2 | #define UTIL_H 3 | 4 | /* logging priorities */ 5 | #include 6 | 7 | /* warning() and error() assume that errno is set */ 8 | void syslog_init(); 9 | void flog(int priority, const char *format, ...); 10 | void warning(const char *prefix); 11 | void error(const char *prefix); 12 | 13 | int vfyhex(int sz, const char *s); 14 | int vfybase32(int sz, const char *s); 15 | 16 | char* alloc_env(const char *var, const char *suffix); 17 | void dealloc_env(char *env); 18 | 19 | /* requires -lrt */ 20 | int rand_init(); 21 | double rand_shift(); 22 | double getmontime(); 23 | void sleepsec(double sec); 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /test/curl: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # Testing mockup script for curl 4 | 5 | error() { 6 | echo curl: "$@" 1>&2 7 | return 1 8 | } 9 | 10 | 11 | # determine staging path 12 | scriptdir="${0%"${0##*/}"}" 13 | cd ${scriptdir:-./}.. 14 | root=${PWD} 15 | 16 | 17 | # Hostnames and their replacement 18 | u1tor=http://`cat ${root}/user1/tor/hidden_service/hostname` 19 | u1i2p=http://`cat ${root}/user1/i2p/eepsite/hostname` 20 | u1rep=http://localhost:9081 21 | 22 | u2tor=http://`cat ${root}/user2/tor/hidden_service/hostname` 23 | u2i2p=http://`cat ${root}/user2/i2p/eepsite/hostname` 24 | u2rep=http://localhost:9082 25 | 26 | 27 | params=`echo x "$@" | sed -r "s/^x //; s@(${u1tor}|${u1i2p})@${u1rep}@g; s@(${u2tor}|${u2i2p})@${u2rep}@g"` 28 | if [ "x ${params}" = "$(echo x "$@")" ]; then 29 | error "502: unknown hostname" 30 | exit 22 31 | fi 32 | 33 | # sleep 0.$((RANDOM * 99 / 32767)) 34 | 35 | eval set -- "${params}" 36 | exec /usr/bin/curl "$@" 37 | -------------------------------------------------------------------------------- /test/logger: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # Testing mockup script for logger 4 | 5 | # parse supported logger arguments 6 | args=`getopt -o t:p: -- "$@"` 7 | eval set -- "${args}" 8 | tag=logger 9 | pri=unspec 10 | 11 | while :; do 12 | case "$1" in 13 | -t) 14 | tag="$2" 15 | shift 2 16 | ;; 17 | 18 | -p) 19 | pri="$2" 20 | shift 2 21 | ;; 22 | 23 | --) 24 | shift 25 | break 26 | ;; 27 | 28 | *) 29 | shift 30 | ;; 31 | esac 32 | done 33 | 34 | echo "[${pri}] ${tag}: $@" 1>&2 35 | -------------------------------------------------------------------------------- /test/oakley-group-2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN DH PARAMETERS----- 2 | MIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE 3 | 3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/ta 4 | iZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgEC 5 | -----END DH PARAMETERS----- 6 | -------------------------------------------------------------------------------- /test/simulate: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | sinfo() { 4 | echo -e "\033[1;33;41m$@\033[0m" 5 | } 6 | 7 | trap '[ $? = 0 ] || kill ${killpid1} ${killpid2} >& /dev/null' 0 8 | killpid1= killpid2= 9 | 10 | 11 | scriptdir="${0%"${0##*/}"}" 12 | cd ${scriptdir:-./}.. 13 | 14 | version="LIBERTE CABLE 3.0" 15 | 16 | 17 | root=${TMPDIR}/stage 18 | sinfo "Root dir: ${root}" 19 | rm -rf ${root} 20 | unset TMPDIR 21 | 22 | 23 | sinfo "Building" 24 | CFLAGS="-O2 -march=core2 -mfpmath=sse -fomit-frame-pointer -pipe -g -UNDEBUG -DTESTING" \ 25 | LDFLAGS="-Wl,-O1,--as-needed,-z,combreloc" make 26 | 27 | 28 | sinfo "Installing stage1" 29 | make -s PREFIX=${root}/stage1 install 30 | 31 | 32 | sinfo "Setting stage1 paths for user1" 33 | cat >> ${root}/stage1/etc/cable/profile <> ${root}/stage2/etc/cable/profile <&1 \ 112 | | sed "s/\<${version}\>/VEROK/; s/.*: //" 113 | } 114 | 115 | csexec() { 116 | local user=`cat ${root}/user1/cable/certs/username` 117 | local req="$1" 118 | shift 119 | 120 | direxec /${user}"${req}" "$@" 121 | } 122 | 123 | wsgrep() { 124 | grep -qw "^${1}.\?\$" 125 | } 126 | 127 | # authentication and basic requests 128 | direxec / -d x | wsgrep 405 129 | direxec / | wsgrep 403 130 | direxec /`cat ${root}/user2/cable/certs/username` | wsgrep 403 131 | csexec "" | wsgrep 403 132 | 133 | csexec /certs/ | wsgrep 403 134 | csexec /certs/xx | wsgrep 403 135 | csexec /request | wsgrep 403 136 | csexec /request/ | wsgrep 400 137 | csexec /request/ver | wsgrep VEROK 138 | csexec /request/ver -I | wsgrep 'HTTP/1.1 200 OK' 139 | csexec /request/ver/ | wsgrep 400 140 | 141 | 142 | # certs fetching 143 | csexec /certs/ca.pem -o ${root}/ca.pem.tmp 144 | csexec /certs/verify.pem -o ${root}/verify.pem.tmp 145 | cmp ${root}/user1/cable/certs/ca.pem ${root}/ca.pem.tmp 146 | cmp ${root}/user1/cable/certs/verify.pem ${root}/verify.pem.tmp 147 | rm ${root}/{ca,verify}.pem.tmp 148 | 149 | chmod u-r ${root}/user1/cable/certs/ca.pem 150 | csexec /certs/ca.pem | wsgrep 404 151 | chmod u+r ${root}/user1/cable/certs/ca.pem 152 | 153 | 154 | # message and key fetching 155 | mid1=1111111111aaaaaaaaaa9999999999ffffffffff 156 | mid2=0000000000bbbbbbbbbb7777777777eeeeeeeeee 157 | 158 | csexec /queue/${mid1} | wsgrep 404 159 | csexec /queue/${mid1}.key | wsgrep 404 160 | csexec /rqueue/${mid2} | wsgrep 403 161 | csexec /rqueue/${mid2}.key | wsgrep 404 162 | csexec //queue/${mid1} | wsgrep 403 163 | csexec /rqueue//${mid2}.key | wsgrep 403 164 | csexec /queue/${mid1}0 | wsgrep 403 165 | csexec /queue/${mid1/a/A} | wsgrep 403 166 | csexec /qqueue/${mid1}.keyx | wsgrep 403 167 | csexec /rqueue/${mid2}.keyx | wsgrep 403 168 | 169 | mkdir ${root}/user1/queues/queue/${mid1} ${root}/user1/queues/rqueue/${mid2} 170 | echo test1 > ${root}/user1/queues/queue/${mid1}/message.enc 171 | echo test2 > ${root}/user1/queues/queue/${mid1}/speer.sig 172 | echo test3 > ${root}/user1/queues/rqueue/${mid2}/rpeer.sig 173 | 174 | csexec /queue/${mid1} -o ${root}/message.enc.tmp 175 | csexec /queue/${mid1}.key -o ${root}/speer.sig.tmp 176 | csexec /rqueue/${mid2}.key -o ${root}/rpeer.sig.tmp 177 | cmp ${root}/user1/queues/queue/${mid1}/message.enc ${root}/message.enc.tmp 178 | cmp ${root}/user1/queues/queue/${mid1}/speer.sig ${root}/speer.sig.tmp 179 | cmp ${root}/user1/queues/rqueue/${mid2}/rpeer.sig ${root}/rpeer.sig.tmp 180 | 181 | chmod u-r ${root}/user1/queues/queue/${mid1}/message.enc 182 | csexec /queue/${mid1} | wsgrep 404 183 | 184 | rm -r ${root}/{message.enc,{s,r}peer.sig}.tmp \ 185 | ${root}/user1/queues/queue/${mid1} ${root}/user1/queues/rqueue/${mid2} 186 | 187 | 188 | # web service 189 | host1=o7te4msv3iexije6.onion 190 | host2=rorhxd3mqkngsj4m4y53jv42tfp2fd4bl4w7jbdeitnxw65wweaa.b32.i2p 191 | user1=25ebhnuidr6sbporsuhm43tig6oj2moo 192 | mac1=13c3fc33754e4266df492a43f8aa72a82e2ef55eb0f20051da98c8c96dfd0cef576e895b9a61211a5ee1b3057999e56db1b6ff39d5502963c0266095e4c62612 193 | mac2=6a799bb1b80087cc23f5955551f2e56c08f69287f87fb59fdb21251d912b8d6be8791f7528f82fe7ab453f432b04ac9859d8524d01740810d87c1c6a19781e97 194 | 195 | 196 | # oversize long request 197 | csexec /request/${mac1}${mac1}${mac1}${mac1} | wsgrep 400 198 | 199 | # incorrect request formats 200 | csexec /request/msg/${mid1//f/F}/${host1}/${user1} | wsgrep 400 201 | csexec /request/msg/${mid1}/${host1//3/1}/${user1} | wsgrep 400 202 | csexec /request/msg/${mid1}/${host2//3/8}/${user1} | wsgrep 400 203 | csexec /request/msg/${mid1}/${host1//3/_}/${user1} | wsgrep 400 204 | csexec /request/msg/${mid1}/${host1//3/{}/${user1} | wsgrep 400 205 | csexec /request/msg/${mid1}/${host2}/${user1//o/O} | wsgrep 400 206 | csexec /request/msg/${mid1}/${host1}/${user1}/extra | wsgrep 400 207 | csexec /request/snd/${mid1//f/g}/${mac1} | wsgrep 400 208 | csexec /request/rcp/${mid1}/${mac1//9/A} | wsgrep 400 209 | csexec /request/rcp/${mid1}/${mac1//9/:} | wsgrep 400 210 | csexec /request/rcp/${mid1}/${mac1//9//} | wsgrep 400 211 | csexec /request/rcp/${mid1}/${mac1//9/_} | wsgrep 400 212 | csexec /request/rcp/${mid1}/${mac1//9/g} | wsgrep 400 213 | csexec /request/ack/${mid1}/${mac1}0 | wsgrep 400 214 | csexec /request/snd/${mid1}/${mac1}/extra | wsgrep 400 215 | csexec /request/ver/extra | wsgrep 400 216 | csexec /request//ver | wsgrep 400 217 | csexec /request/ver/ | wsgrep 400 218 | csexec /request/snd/${mid1}//${mac1} | wsgrep 400 219 | 220 | 221 | # msg request (mid1, host1, user1) 222 | csexec /request/msg/${mid1}/${host1}/${user1} | wsgrep VEROK 223 | [ -e ${root}/user1/queues/rqueue/${mid1}/peer.req ] 224 | 225 | # repeated msg request (mid1, host1, user1) [ok and skip if exists] 226 | csexec /request/msg/${mid1}/${host1}/${user1} | wsgrep VEROK 227 | 228 | # simultaneous msg request (mid1, host2, user1) 229 | mv ${root}/user1/queues/rqueue/${mid1}{,.new} 230 | csexec /request/msg/${mid1}/${host2}/${user1} lock ${root}/user1/queues/rqueue/${mid1}.new/ | wsgrep 500 231 | [ "`cat ${root}/user1/queues/rqueue/${mid1}.new/hostname`" = ${host1} ] 232 | 233 | # repeated msg request after failed msg (mid1, host2, user1) 234 | csexec /request/msg/${mid1}/${host2}/${user1} | wsgrep VEROK 235 | [ "`cat ${root}/user1/queues/rqueue/${mid1}/hostname`" = ${host2} ] 236 | 237 | 238 | # too early snd request (mid1, mac1) [check peer.ok] 239 | csexec /request/snd/${mid1}/${mac1} | wsgrep 500 240 | [ ! -e ${root}/user1/queues/rqueue/${mid1}/send.mac ] 241 | 242 | # simultaneous snd request (mid1, mac1) [fail if locked] 243 | touch ${root}/user1/queues/rqueue/${mid1}/peer.ok 244 | csexec /request/snd/${mid1}/${mac1} lock ${root}/user1/queues/rqueue/${mid1}/ | wsgrep 500 245 | [ ! -e ${root}/user1/queues/rqueue/${mid1}/send.mac ] 246 | [ ! -e ${root}/user1/queues/rqueue/${mid1}/recv.req ] 247 | 248 | # snd request (mid1, mac1) 249 | csexec /request/snd/${mid1}/${mac1} | wsgrep VEROK 250 | [ "`cat ${root}/user1/queues/rqueue/${mid1}/send.mac`" = ${mac1} ] 251 | [ ${root}/user1/queues/rqueue/${mid1}/recv.req -ef ${root}/user1/queues/rqueue/${mid1}/peer.ok ] 252 | 253 | # repeated snd request (mid1, mac2) [write send.mac: skip if exists] 254 | csexec /request/snd/${mid1}/${mac2} | wsgrep VEROK 255 | [ "`cat ${root}/user1/queues/rqueue/${mid1}/send.mac`" = ${mac1} ] 256 | 257 | 258 | # too early rcp request (mid2, mac1) [check send.ok] 259 | mkdir ${root}/user1/queues/queue/${mid2} 260 | echo ${mac1} > ${root}/user1/queues/queue/${mid2}/recv.mac 261 | csexec /request/rcp/${mid2}/${mac1} | wsgrep 500 262 | [ ! -e ${root}/user1/queues/queue/${mid2}/ack.req ] 263 | 264 | # simultaneous rcp request (mid2, mac1) [fail if locked] 265 | touch ${root}/user1/queues/queue/${mid2}/send.ok 266 | csexec /request/rcp/${mid2}/${mac1} lock ${root}/user1/queues/queue/${mid2}/ | wsgrep 500 267 | [ ! -e ${root}/user1/queues/queue/${mid2}/ack.req ] 268 | 269 | # rcp request (mid2, mac1) 270 | csexec /request/rcp/${mid2}/${mac1} | wsgrep VEROK 271 | [ ${root}/user1/queues/queue/${mid2}/ack.req -ef ${root}/user1/queues/queue/${mid2}/send.ok ] 272 | 273 | # incorrect rcp request (mid2, mac2) [compare recv.mac] 274 | rm ${root}/user1/queues/queue/${mid2}/ack.req 275 | csexec /request/rcp/${mid2}/${mac2} | wsgrep 500 276 | [ ! -e ${root}/user1/queues/queue/${mid2}/ack.req ] 277 | 278 | 279 | # too early ack request (mid1, mac1) [check recv.ok] 280 | rm ${root}/user1/queues/rqueue/${mid1}/* 281 | echo ${mac1} > ${root}/user1/queues/rqueue/${mid1}/ack.mac 282 | csexec /request/ack/${mid1}/${mac1} | wsgrep 500 283 | [ -e ${root}/user1/queues/rqueue/${mid1} ] 284 | 285 | # simultaneous ack request (mid1, mac1) [fail if locked] 286 | touch ${root}/user1/queues/rqueue/${mid1}/recv.ok 287 | csexec /request/ack/${mid1}/${mac1} lock ${root}/user1/queues/rqueue/${mid1}/ | wsgrep 500 288 | [ -e ${root}/user1/queues/rqueue/${mid1} ] 289 | 290 | # ack request (mid1, mac1) 291 | csexec /request/ack/${mid1}/${mac1} | wsgrep VEROK 292 | [ -e ${root}/user1/queues/rqueue/${mid1}.del ] 293 | 294 | # incorrect ack request (mid1, mac2) [compare ack.mac] 295 | mv -T ${root}/user1/queues/rqueue/${mid1}{.del,} 296 | csexec /request/ack/${mid1}/${mac2} | wsgrep 500 297 | [ -e ${root}/user1/queues/rqueue/${mid1} ] 298 | 299 | rm -r ${root}/user1/queues/rqueue/${mid1} ${root}/user1/queues/queue/${mid2} 300 | 301 | 302 | sinfo "Testing cable-id and cable-ping" 303 | for u in 1 2; do 304 | for tp in user tor i2p; do 305 | eval u${u}${tp}=`${root}/stage${u}/bin/cable-id ${tp}` 306 | done 307 | done 308 | 309 | for u in 1 2; do 310 | for tp in tor i2p; do 311 | eval cpresp=\`${root}/stage1/bin/cable-ping \${u${u}user}@\${u${u}${tp}}\` 312 | [ "${cpresp}" = "${version}" ] 313 | done 314 | done 315 | 316 | 317 | sinfo "Testing synchronous message flow (fetch errors are expected)" 318 | ccsend() { 319 | local u="$1" 320 | local desc="$2" 321 | local from="$3" 322 | local to1="$4" 323 | local to2="$5" 324 | 325 | ${root}/stage${u}/bin/cable-send < 327 | To: Anon Anon <${to1}>, Anon Anon <${to2}> 328 | Subject: Test (${desc}) 329 | 330 | Test 331 | EOF 332 | } 333 | 334 | ccsend 1 "Tor1 -> Tor2, I2P2 (loop)" ${u1user}@${u1tor} ${u2user}@${u2tor} ${u2user}@${u2i2p} 335 | ccsend 1 "I2P1 -> Tor2, I2P2 (loop)" ${u1user}@${u1i2p} ${u2user}@${u2tor} ${u2user}@${u2i2p} 336 | 337 | ccloop() { 338 | local u="$1" 339 | local mid="$2" 340 | if [ ${u} = 1 ]; then 341 | local q=queue 342 | else 343 | local q=rqueue 344 | fi 345 | 346 | ( 347 | . ${root}/stage${u}/etc/cable/profile 348 | ${CABLE_HOME}/loop ${q} ${mid} 349 | ) 350 | } 351 | 352 | sed -i 's/\<22\>/222/' ${root}/stage{1,2}/libexec/cable/fetch 353 | mids=`find ${root}/user1/queues/queue -mindepth 1 -maxdepth 1 -printf '%P\n'` 354 | for mid in ${mids}; do 355 | ccloop 1 ${mid} || : 356 | ccloop 2 ${mid} 357 | ccloop 1 ${mid} 358 | ccloop 2 ${mid} 359 | ccloop 1 ${mid} 360 | ccloop 1 ${mid}.del 361 | ccloop 2 ${mid}.del 362 | done 363 | sed -i 's/\<222\>/22/' ${root}/stage{1,2}/libexec/cable/fetch 364 | 365 | kill ${killpid1} ${killpid2} 366 | wait ${killpid1} ${killpid2} || : 367 | killpid1= killpid2= 368 | 369 | 370 | sinfo "Testing non-inotify daemon operation" 371 | maxname=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 372 | ccdaemon() { 373 | local pid1= pid2= chkdir= 374 | 375 | # tests ability to deal with max-length filenames (readdir and inotify) 376 | mkdir ${root}/user{1,2}/queues/{,r}queue/${maxname} 377 | 378 | ${root}/stage1/libexec/cable/cabled & pid1=$! 379 | ${root}/stage2/libexec/cable/cabled & pid2=$! 380 | 381 | for chkdir in user{1,2}/queues; do 382 | while find ${root}/${chkdir} -mindepth 2 -maxdepth 2 ! -name ${maxname} | grep -q .; do 383 | sleep 2 384 | done 385 | done 386 | 387 | kill ${pid1} ${pid2} 388 | wait ${pid1} ${pid2} || : 389 | 390 | rmdir ${root}/user{1,2}/queues/{,r}queue/${maxname} 391 | if find ${root} -path '*queue/*' | grep -q .; then 392 | echo "queue leftovers" 393 | return 1 394 | fi 395 | } 396 | 397 | # tests lock starvation 398 | ccsend 1 "Tor1 -> I2P1 (self-daemon)" ${u1user}@${u1tor} ${u1user}@${u1i2p} 399 | ccsend 2 "I2P2 -> Tor2 (self-daemon)" ${u2user}@${u2i2p} ${u2user}@${u2tor} 400 | CABLE_NOWATCH=1 ccdaemon 401 | 402 | 403 | sinfo "Testing daemon operation" 404 | 405 | ccsend 1 "Tor1 -> Tor2, I2P2 (daemon)" ${u1user}@${u1tor} ${u2user}@${u2tor} ${u2user}@${u2i2p} 406 | ccsend 1 "I2P1 -> Tor2, I2P2 (daemon)" ${u1user}@${u1i2p} ${u2user}@${u2tor} ${u2user}@${u2i2p} 407 | ccsend 2 "Tor2 -> Tor1, I2P1 (daemon)" ${u2user}@${u2tor} ${u1user}@${u1tor} ${u1user}@${u1i2p} 408 | ccsend 2 "I2P2 -> Tor1, I2P1 (daemon)" ${u2user}@${u2i2p} ${u1user}@${u1tor} ${u1user}@${u1i2p} 409 | ccsend 1 "Tor1 -> Tor1, I2P1 (daemon)" ${u1user}@${u1tor} ${u1user}@${u1tor} ${u1user}@${u1i2p} 410 | ccsend 2 "I2P2 -> Tor2, I2P2 (daemon)" ${u2user}@${u2i2p} ${u2user}@${u2tor} ${u2user}@${u2i2p} 411 | 412 | # stale temporary directories removal 413 | faketmp=`mktemp -d --tmpdir=${root}/user1/queues/queue` 414 | touch ${faketmp}/test{1,2} 415 | fakemid=${root}/user1/queues/rqueue/012345abcd012345abcd012345abcd012345abcd.new 416 | cp -r ${faketmp} ${fakemid} 417 | touch --date="2 days ago" ${faketmp} ${fakemid} 418 | 419 | (sleep 10; touch -c ${root}/user{1,2}/queues/{,r}queue/${maxname}) & 420 | ccdaemon 421 | 422 | if grep -rq Failed ${root}/user{1,2}/inbox; then 423 | false 424 | fi 425 | 426 | 427 | sinfo "Testing message expiration" 428 | ccsend 1 "Tor1 -> Tor1, Tor2 (expire)" ${u1user}@${u1tor} ${u1user}@${u1tor} ${u2user}@${u2tor} 429 | ccsend 2 "Tor2 -> Tor2, Tor1 (expire)" ${u2user}@${u2tor} ${u2user}@${u2tor} ${u1user}@${u1tor} 430 | ccsend 2 "Tor1 -> Tor1, Tor2 (expire, fake)" ${u1user}@${u1tor} ${u1user}@${u1tor} ${u2user}@${u2tor} 431 | ccsend 1 "Tor2 -> Tor2, Tor1 (expire, fake)" ${u2user}@${u2tor} ${u2user}@${u2tor} ${u1user}@${u1tor} 432 | 433 | echo CABLE_TMOUT=17 >> ${root}/stage1/etc/cable/profile 434 | echo CABLE_TMOUT=17 >> ${root}/stage2/etc/cable/profile 435 | ccdaemon 436 | 437 | if ! grep -rq Failed ${root}/user{1,2}/inbox; then 438 | false 439 | fi 440 | 441 | 442 | sinfo "Verifying valgrind statistics" 443 | if grep 'in use at exit:' ${root}/valgrind.* | grep -v ' 0 bytes in 0 blocks'; then 444 | false 445 | fi 446 | 447 | if grep 'FILE DESCRIPTORS:' ${root}/valgrind.* | grep -v ' 4 open at exit'; then 448 | false 449 | fi 450 | 451 | 452 | sinfo "Success" 453 | --------------------------------------------------------------------------------