Defaults to localhost:2013
. You should override these before any other calls are made. Note that cross-domain requests may be enabled in the server configuration.
crypto
algorithms:
AES256 CFB and 128 bit segment width (16 bytes)
RSA
KDF (PBKDF2 w/ 50,000 rounds)
generate:
32 random bytes hmac key for container names ("container_name_hmac_key")
32 random bytes hmac key for general data authentication ("hmac_key")
32 random bytes for ("salt_key")
32 random bytes for ("salt_challenge")
32 random bytes for ("symkey")
rsa keypair (2048 bits) (rsa_keypair_obj)
outputs:
challenge_key = kdf(salt_challenge, passphrase)
keypair_key = kdf(salt_key, passphrase)
keypair_as_string = rsa_keypair_obj.serialize_to_string()
pubkey_as_string = rsa_keypair_obj.public_key.serialize_to_string()
keypair_iv = sha256(uuid()).digest()[:16]
cipher = aes256cfb(key=keypair_key, iv=keypair_iv)
# XXX padding (must be length of multiple of 16)
keypair_serialized_ciphertext = cipher.encrypt(keypair_as_string)
symkey_ciphertext = rsa_keypair_obj.encrypt_to_private(symkey)
container_name_hmac_key_iv = sha256(uuid()).digest()[:16]
cipher = aes256cfb(key=symkey, iv=container_name_hmac_key_iv)
container_name_hmac_key_ciphertext = cipher.encrypt(container_name_hmac_key)
hmac_key_iv = sha256(uuid()).digest()[:16]
cipher = aes256cfb(key=symkey, iv=hmac_key_iv)
hmac_key_ciphertext = cipher.encrypt(hmac_key)
steps:
save this object:
{
hmac_key: 32 byte string, base64ed
salt_key: 32 byte string, base64ed
salt_challenge: 32 byte string, base64ed
keypair_iv: 16 byte string, base64ed
keypair_serialized_ciphertext: many bytes, base64ed
pubkey_serialized: many bytes, base64ed
challenge_key: 32 byte string, base64ed
symkey_ciphertext: many bytes, base64ed
container_name_hmac_key_iv: 16 byte string, base64ed
container_name_hmac_key_ciphertext: 32 byte string, base64ed
hmac_key_iv: 16 byte string, base64ed
hmac_key_ciphertext: 32 byte string, base64ed
}
discard:
keypair_key
rsa_keypair_obj
cipher objects
symkey
container_name_hmac_key
hmac_key
keypair_as_string
pubkey_as_string
Creates an account object and generates the appropriate salts and keys.
Checks with the server to validate the username, and calls back with a potentially empty error
argument and a conditional account
argument which must still be save()
d.
crypto
Step 1: get a challenge from the server.
The server constructs a challenge like this:
# make a challenge
random_string = random(32)
time_value = time.time() # any string representing a timestamp will do
challenge_key = challenge_key from account
salt_challenge = salt_challenge from account
aes_iv = sha256(uuid()).digest()[:16]
cipher = aes256cfb(key=challenge_key, iv=iv, segment_width=128)
challenge = cipher.encrypt(random_string)
# compute the answer we antipate from the client
answer_cipher = aes256cfb(key=challenge, iv=iv, segment_width=8)
time_value_ciphertext = cipher.encrypt(time_value)
expected_answer_digest = sha256(time_value_ciphertext).digest()
Persist the challenge to the database in the challenge table, so that we
can check the challenge response when it comes in. Get the challenge_id
back for the inserted row.
challenge_identifier = public_id(challenge_id)
The server returns this as the challenge response:
{
challenge_id: challenge_identifier,
challenge: base64(challenge),
salt_challenge: base64(salt_challenge),
iv: base64(aes_iv),
time_value: time_value
}
Step 2: construct an answer to the challenge
The way this works, is that we prove that given the salt_challenge, we can
calculate challenge_key (using our passphrase) and then make use of it to
encrypt an answer back to the server.
In this case, we're deriving the key, and then encrypting time_value using
that key, and sending the ciphtertext back to the server.
challenge_key = kdf(salt_challenge, passphrase)
cipher = aes256cfb(key=challenge_key, iv=iv, segment_width=128)
challenge = cipher.decrypt(challenge)
cipher = aes256cfb(key=challenge, iv=iv, segment_width=8)
time_value_ciphertext = cipher.encrypt(time_value)
Post an answer to the challenge like this:
{
challenge_id: challenge_id,
answer: base64(time_value_ciphertext),
}
If we have sucessfully answered the challenge, the server should then respond with our account info and a session token.
end_crypto
Performs the necessary handshakes with the server, and calls back with a potentiall empty error
object and a conditional session
argument.
Reconstructs a serialized session and pings the server to check its validity.
Session objects are event emitters. on()
Removes all event listeners for a given eventName
.
Stringifies said session object for later use, and calls back with a potentially empty error
argument and a conditional session
string argument.
Hits the appropriate route on the server to check for the validity of said session. Calls back with a potentially empty error
argument. If the argument is empty, the session is still valid.
crypto _end_crypto
Checks for a cached container
that is available to said session. If the container
is not cached or a more current version is available, the latest version is retreived from the server and cached.
crypto
Attempts to create a container, checking with the server to see if the namespace is available for the current session. Calls back with a potentially empty error
argument.
Container names are encrypted and their plaintext is unknown to the server.
An object containing a representation of the account associated with said session.
Example structure:
{
username: String,
passphrase: String,
keys: { },
save: Function,
refresh: Function,
version: Function
}
crypto diff
Determines the differences between the previous version of the account of said session and saves it with the server. Calls back with a potentially empty error
argument.
crypto?
Checks with the server for new account versions from other devices. Calls back with a potentially empty error
argument. If the argument is empty, the account has been updated.
Holds the current version hash of the session.
Transactions are how all data is moved to the server. All save()
methods transparently construct a transaction and commit it.
An array holding the save()
d chunks of the transaction.
Adds a chunk of data to the server. Accepts an abitrary amount of arguments containing containers and/or their objects in any format.
crypto diff
Finalizes the transaction, determines differences in containers, encrypts the data, and sends it to the server. Calls back with a potentially empty error
argument. If the argument is empty, the transaction has been committed.
crypto undiff
crypto diff
Determines the differences with the previously saved version of the container. Calls back with a potentially empty error
argument. If the argument is empty, the container has been committed to the server.
Adds a magic key to said container.
Holds the current version hash of the container.
Hits the server for a list of known version identifiers. Calls back with a potentially empty error
argument and conditionally a history
argument containing an array of version identifiers.
Constructs a Diff object containing the changes with the last known version of the container. Calls back with a potentially empty error
argument, and conditionally a diff
argument.
function uuid_factory() {
// build a function that generates sequential uuids that are difficult to
// cause collisions, but don't require constant streoam of new entropy
private_counter = 1
initial_timestamp = timestamp
initial_random = random(32)
function uuid2() {
private_counter += 1
uuid = sha256(initial_random + initial_timestamp + private_counter).hexdigest()
return uuid
}
return uuid2
}
// give us a uuid closure
uuid = uuid_factory()
// this is memoizable based on peer name or maybe peer private key id
function session_key_ciphertext_for_peer(peer) {
session_key = random(32)
hmac_key = random(32)
session_key_ciphertext = peer.publickey.encrypt(session_key)
hmac_key_ciphertext = peer.publickey.encrypt(hmac_key)
return {session_key, session_key_ciphertext, hmac_key_ciphertext}
}
session_key_ciphertext = session_key_ciphertext_for_peer(peer)
# how to send the first message...
headers_iv = sha256(uuid()).digest()[:16]
body_iv = sha256(uuid()).digest()[:16]
headers = {}
headers_plaintext = zlib.compress(json.stringify(headers))
headers_cipher = aes256cfb(key=session_key, iv=headers_iv)
headers_ciphertext = headers_cipher.encrypt(headers_plaintext)
body = {}
body_plaintext = zlib.compress(json.stringify(body))
body_cipher = aes256cfb(key=session_key, iv=body_iv)
body_ciphertext = body_cipher.encrypt(body_plaintext)
message_signature_hash = sha256(headers_ciphertext + headers_plaintext).digest()
message_signature = account.private_key.sign(message_signature_hash)
post this:
{
peer_name: ...
session_key_ciphertext:
headers_iv:
body_iv:
headers_ciphertext:
body_ciphertext:
message_signature:
}
The server is a simple REST server running on node. The default all bodies are sent and received with JSON. The default success response is:
{
success: true
}
Creates a new account with client-generated data.
Required body:
{
}
Sets session_identifier cookie (logs you in immediately) upon successful request.
Logs into account and sets session_identifier
cookie.
Required body:
{
}
Changes the password for an account.
Required body:
{
password: String
}
Pings the server to verify that the session is still valid. Must send session_identifier
cookie.
If the session is invalid when an authentication-requiring route is requested, the default response will be:
{
success: false,
error: "Not logged in"
}
Generates and sets transaction_token
cookie.
Requires session_identifier
cookie.
Commit (finalize) the transaction.
Requires session_identifier
cookie.
May return the following:
{
success: false,
error: "Transaction token invalid"
}
Cancel a transaction without committing it to the server.
Requires session_identifier
cookie.
May return the following:
{
success: false,
error: "Transaction token invalid"
}
Returns all headers of the records in the container.
Requires session_identifier
cookie.
Optional parameter ?after=record_version_identifier
will only return the headers for records occuring after said record_version_identifier
Example:
{
}
multipart/form-upload
of json + payload for this modification
// TODO fail early if the transaction is borked
Requires session_identifier
cookie.
A valid transaction token is required or the route will return the following:
{
success: false,
error: "Transaction token invalid"
}
Returns binary data of the ciphertext from the given record_version_identifier
of the enciphered container_name
.
Requires session_identifier
cookie.
Returns list of message headers as JSON objects.
Requires session_identifier
cookie.
Optional parameters of from=username
and since=timestamp
may be used to filter.
Example response:
{
}
Returns headers and ciphtertext of payload of message with matching message_identifier
Requires session_identifier
cookie.
Example response:
{
}
Deletes a given message by message_identifier
Requires session_identifier
cookie.
May return the following:
{
success: false,
error: "Transaction token invalid"
}
Send a message by multipart/form-upload
of json + payload
// TODO fail early if the transaction is borked
Requires session_identifier
cookie.
Example post data:
// TODO decide on format
Requires session_identifier cookie.
May return the following:
{
success: false,
error: "Transaction token invalid"
}