Skip to content

Instantly share code, notes, and snippets.

@pdlan
Created January 2, 2020 21:07
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save pdlan/81245e3d83d7fa586b8dcbb4e2d507cc to your computer and use it in GitHub Desktop.
Save pdlan/81245e3d83d7fa586b8dcbb4e2d507cc to your computer and use it in GitHub Desktop.
RA2ne Security Type Specification

RA2ne Security Type Specification

ServerPublicKey

After RA2ne security type is selected server sends its RSA public key of 2048 bits. Note that the modulus and the public exponent are big-endian big integers.

No. of bytes Type Value Description
4 U32 2048 Key length in bits
256 U8 array Modulus (n)
256 U8 array Public exponent (e)

ClientPublicKey

Client also sends its public key.

No. of bytes Type Value Description
4 U32 2048 Key length in bits
256 U8 array Modulus (n)
256 U8 array Public exponent (e)

ServerRandom

Server generates a random number of 16 bytes and encrypts it with the client's public key. EME-PKCS1-v1_5 padding algorithm is used. Then server sends the encrypted random number.

No. of bytes Type Value Description
2 U16 256 Length of encrypted message
256 U8 array Encrypted random number

ClientRandom

Client also generates a random number of 16 bytes, encrypts it with the server's public key and then sends it.

No. of bytes Type Value Description
2 U16 256 Length of encrypted message
256 U8 array Encrypted random number

Server's random number is decrypted with the client's private key. At the same time, client's random number is decrypted with the server's private key. Now both random numbers are known by both sides. Client session key and server session key can be derived as follows.

ClientSessionKey = the first 16 bytes of SHA1(ServerRandom || ClientRandom)
ServerSessionKey = the first 16 bytes of SHA1(ClientRandom || ServerRandom)
("||" means concatenation)

ClientSessionKey is used to encrypted the message from client to server. ServerSessionKey is used to encrypted the message from server to client.

After that encrypted messages will be encrypted with AES-CTR and will be sent followed by a message authentication code. Every time a message is encrypted, a new IV is generated from the session key and the value of a 16-byte counter of the messages. Then the encrypted message is used to generate a MAC. The new value of the counter is computed for the encryption of the next time. When client or server receives an encrypted message, it's supposed to authenticate the message. If the authentication fails, the connection should be closed. Reference implementation of the algorithm is as follows.

class RA2Cipher {
    constructor(key) {
        // PRNG is used to generate IV for every encrypted message.
        // session key is also used as PRNG's seed.
        this._key = key;
        this._aes = new aesjs.AES(this._key);
        this._counter = new Uint8Array(16);
        this._iv = null;
        // a and b are parameters for PRNG.
        this._a = this._aes.encrypt(new Uint8Array(16));
        this._b = new Uint8Array(16);
        const v = this._a[0] >> 6;
        for (let i = 0; i < 15; i++) {
            this._b[i] = (this._a[i + 1] >> 6) | (this._a[i] << 2);
            this._a[i] = (this._a[i + 1] >> 7) | (this._a[i] << 1);
        }
        this._b[14] ^= v >> 1;
        const magic_numbers = new Uint8Array([0x0, 0x87, 0x0e, 0x89]);
        this._b[15] = 4 * this._a[15] ^ magic_numbers[v];
        this._a[15] = 2 * this._a[15] ^ magic_numbers[v >> 1];
        
    }

    encrypt(plaintext_msg) {
        this._compute_iv();
        let ctr = new aesjs.ModeOfOperation.ctr(this._key, new aesjs.Counter(this._iv));
        const encrypted = ctr.encrypt(plaintext_msg);
        const mac = this._mac(encrypted);
        let msg = new Uint8Array(encrypted.length + mac.length);
        msg.set(encrypted);
        msg.set(mac, encrypted.length);
        for (let i = 0; i < 16 && this._counter[i]++ == 255; i++);
        return msg;
    }

    decrypt(msg) {
        this._compute_iv();
        const encrypted = msg.subarray(0, msg.length - 16);
        const mac = this._mac(encrypted);
        for (let i = 0; i < 16; i++) {
            if (mac[i] != msg[msg.length - 16 + i]) {
                return undefined; // failed to authenticate the message
            }
        }
        let ctr = new aesjs.ModeOfOperation.ctr(this._key, new aesjs.Counter(this._iv));
        const decrypted = ctr.decrypt(encrypted);
        for (let i = 0; i < 16 && this._counter[i]++ == 255; i++);
        return decrypted;
    }

    _compute_iv() {
        this._iv = this._aes.encrypt(new Uint8Array(16));
        for (let i = 0; i < 16; i++) {
            this._iv[i] ^= this._counter[i] ^ this._a[i];
        }
        this._iv = this._aes.encrypt(this._iv);
    }

    _mac(encrypted, iv) {
        let c = this._aes.encrypt(new Uint8Array([
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
        ]));
        c[0] ^= (encrypted.length & 0xff00) >> 8;
        c[1] ^= encrypted.length & 0xff;
        c[2] ^= 0x80;
        for (let i = 0; i < 16; i++) {
            c[i] ^= this._b[i];
        }
        c = this._aes.encrypt(c);
        // compute the last block of CFB
        let buff = this._aes.encrypt(new Uint8Array([
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2
        ]));
        for (let i = 0, j = 0; i < encrypted.length; i++) {
            if (j == 16) {
                buff = this._aes.encrypt(buff);
                j = 1;
                buff[0] ^= encrypted[i]
            } else {
                buff[j] ^= encrypted[i];
                j++;
            }
        }
        if (encrypted.length % 16 != 0) {
            buff[encrypted.length % 16] ^= 0x80;
            for (let i = 0; i < 16; i++) {
                buff[i] ^= this._b[i];
            }
        } else {
            for (let i = 0; i < 16; i++) {
                buff[i] ^= this._a[i];
            }
        }
        buff = this._aes.encrypt(buff);
        let out = new Uint8Array(16);
        for (let i = 0; i < 16; i++) {
            out[i] = this._iv[i] ^ buff[i] ^ c[i];
        }
        return out;
    }
}

ServerHash

Server sends encrypted server hash.

ServerHash = SHA1(ServerPublicKey || ClientPublicKey)
Note that ServerPublicKey and ClientPublicKey are both of 516 bytes
No. of bytes Type Value Description
2 U16 20 Length of ServerHash
20 U8 array Encrypted ServerHash
16 U8 array MAC

ClientHash

Client sends encrypted client hash.

ClientHash = SHA1(ClientPublicKey || ServerPublicKey)
No. of bytes Type Value Description
2 U16 20 Length of ClientHash
20 U8 array Encrypted ClientHash
16 U8 array MAC

After decryption, server should compare received ClientHash with the one it computes itself. Vice versa.

RA2Subtype

Server sends subtype to let client know what credentials are needed.

No. of bytes Type Value Description
2 U16 1 Length of RA2Subtype
1 U8 array Encrypted RA2Subtype
16 U8 array MAC

RA2Subtype is 1 if both username and password are needed. Or it's 2 if only password is needed. Other values are invalid.

Crendentials

Client sends login credentials.

No. of bytes Type Description
2 U16 Length of crendentials
Length of crendentials U8 array Encrypted crendentials
16 U8 array MAC

Plaintext message of the credentials is:

No. of bytes Type Description
1 U8 Length of username (is zero if subtype is 2)
Length of username (can be zero) U8 array Username
1 U8 Length of password
Length of password U8 array Password

After receiving the credentials, the server continues with the SecurityResult message.

@pdlan
Copy link
Author

pdlan commented Mar 25, 2020

RA2 security type is almost the same except that if it is chosen, not only credentials but also all the following messages will be encrypted. Note that a RA2 encrypted message is not necessarily a RFB message, vice versa. That is to say RA2 encryption layer is an independent layer between the TCP layer and the RFB layer.

For RA2_256 (129):

ClientSessionKey = SHA256(ServerRandom || ClientRandom)
ServerSessionKey = SHA256(ClientRandom || ServerRandom)
ServerHash = SHA256(ServerPublicKey || ClientPublicKey)
ClientHash = SHA256(ClientPublicKey || ServerPublicKey)

The encryption algorithm is the same, except that key length is 32 bytes.

@Neustradamus
Copy link

@pdlan: Can you add it in UltraVNC?

@pdlan
Copy link
Author

pdlan commented Oct 29, 2021

@Neustradamus Sorry I'm not familiar with UltraVNC's source tree. But I think this documentation is sufficient if you want to implement it by yourself.

@pdlan
Copy link
Author

pdlan commented Nov 15, 2021

@pdlan: Can you add it in UltraVNC?

Also I may submit a pull request of RA2ne to noVNC again if the recent Apple Remote Desktop authentication PR gets merged and the forge.js dependence is added.

@pdlan
Copy link
Author

pdlan commented Dec 8, 2021

The documentation for RA2(ne|r)(_256)? has been merged into rfbproto.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment