Related Work
Here's other directions we've looked at for potential use in NostrMail.
NIP-05¶
Note
This is not required by NostrMail. This section is just kept for pedagogical purposes.
nip-05 is a Pub key validation standard based on control over a domain. Most email users will not have their own servers, however, so NostrMail clients should not require it.
HelloJessica is nip05 compliant, so we should be able to a get request to his server to verify his pub key
from nostrmail.utils import validate_nip05
validate_nip05(node_hello_hex) # returns the name of this user according to their .com
Hello Jessica's NIP-05 json actually includes several other names, so this doubles as a PGP registry. However, nip-02
provides a similar solution to registration.
NIP-02¶
NIP-02 Supports contacts lists, comprised of pub keys, petnames, and preferred relays. Users may be found by walking through the network of contacts. This is desirable for nostrmail, where we want to easily look up an email address through dot notation. For instance, an email to carol.bob.alice
means find carol in my contacts
, then find bob in carol's contacts
, then find alice in bob's contacts
.
Fernet encryption¶
Note
NostrMail does not use this method. We decided to use the same scheme as NOSTr DMs to reduce the workload on other implementations.
We could have used Fernet encryption available from the cryptography package. Fernet encryption is a form of symmetric encryption, meaning the same key may be used to encrypt and decrypt a message.
from cryptography.fernet import Fernet, InvalidToken
import base64
def get_fernet(key):
if isinstance(key, str):
fernet_key = base64.urlsafe_b64encode(bytes(key.ljust(32).encode()))
else:
fernet_key = base64.urlsafe_b64encode(key)
return Fernet(fernet_key)
def encrypt(message, key):
f = get_fernet(key)
token = f.encrypt(message.encode())
encrypted_msg = token.decode('ascii')
return encrypted_msg
def decrypt(message, key):
f = get_fernet(key)
decrypted_msg = f.decrypt(message.encode()).decode('ascii')
return decrypted_msg
decrypt(encrypt('hello world', 'yowzah'), 'yowzah')
While apparently simpler, there are a few drawbacks that make this untennable:
- There's no
iv
token as with AES, so you have to directly match the dm with the subject when looking up emails - All NostrMail clients would have to implement fernet encryption in addition to AES for dms
TOTP¶
We may use a different key for each message by concatonating the shared secret with a time stamp and hashing the result. This is known as a time-based on-time password (TOTP) and should already be familiar to anyone who has used google authenticator. The time used would be the time the email was sent. The epoch needs to be large enough for the mail servers to route the message.
It might also help to use the latest block hash as the time stamp.
This approach may provide some additional security benefit, such as mitigating replay attacks or preventing emails from being sent from the future or something.
from cryptography.hazmat.primitives import hashes
def sha256(message):
digest = hashes.Hash(hashes.SHA256())
digest.update(message.encode())
digest.update(b"123")
return digest.finalize()
import base64
base64.urlsafe_b64encode(sha256('hey')).decode('ascii')
sha256('hey')
def hash_concat(key, value):
"""concatonates a message with a value and returns the hash
key - a binary
"""
key_str = base64.urlsafe_b64encode(key).decode('ascii')
return sha256(key_str + str(value))
Using the most recent bitcoin block
latest_block_hash = '000000000000000000065a582c53ef20e5ae37b74844b31bfcbd82f4c515fdb2'
epoch_value = latest_block_hash
assert sender_secret == receiver_secret
print(decrypt(encrypt(email_msg,
hash_concat(sender_secret, latest_block_hash)),
hash_concat(receiver_secret, epoch_value)) #
)