中文版点击这里

neoatlantis-crypto-js is the encryption library implemented under JavaScript written by me with a complete set of features. This article will lead you to the usage of such features.

Because of the rich content inside the library, and the active development and debugging, this article will be continously updated. The title indicates the version number. A chinese version is available.

0 Motivation

0.1 enigma system

This library is a result of developing the enigma system. To understand this library, the enigma system is firstly briefly described. Enigma system is designed as a service running on embedded systems:

  • For requests inputed from the outside of the embedded system via API, it does following operations:
    • display the request to the user, ask for approval, encrypt and/or sign the data within the request, and return the result to the outside.
    • treat the input as ciphertext or signed plaintext from another enigma, decrypt the content and validate it, display the result to the user, and return the result to the outside under user approval.
    • under request and user approval, transfer the identity public key to the outside.
    • under request and user approval, improve an identity public key from other user.
  • Besides, users are able to do following operations with interacting with IO devices connected to the embedded system, e.g. keyboards, mouses, screens:
    • manage(including listing, generating, deleting, importing, exporting) the stored identity public keys(public and secret) within the embedded system.
    • edit the drafts for sending. When approving requests from the outside, use drafts to replace given data.
    • view the received and stored decrypted data.
    • edit and save a type of special drafts, which is the signature on the dentity keys of the others(named token).

Engima system serves as the core component of many communicating tools and protocols. For example, in cooperation with plugins in instant messaging softwares, it provides peer-to-peer(p2p) encryption. It may also works as an component of automatic doors. By signing the given challenge, user proves its identity to the door.

0.2 neoatlantis-crypto-js library

In developing the engima system, the author realized, that it is necessary to separate the cryptographic part of this system into one library, in which features like secure random generators, symmetric encryptions, asymmetric encryptions and signatures, hash functions are covered. The library covers also 2 data structures for constructing the engima system: identity key and message.

Another highlight point of this library is, considering that even though these 2 data structures are included, it is still not so easy to construct an enigma system since one have to deal with storage and the complex logic reading the ciphertext, there is an interface, which is in fact a nearly complete implementation of the enigma system. The job of programmers are therefore now only to ask the user with user interface with questions emitted from this library, and the output is there. Using this implementation, the programmer shall construct a complete or reduced engima system, retaining the compatibility.

In short, the neoatlantis-crypto-js library provides user not only the basic features doing cryptography, but also the rich logic prepared for peer-to-peer encryption.

0.3 list of features

Here is a complete list of features shipped together with this library.

  • Hashing
    • with algorithms of BLAKE2s、WHIRLPOOL、RIPEMD160,
    • and use them to calculate MAC(Message Authentication Code) of some message,
    • and use them to do PBKDF2 key derivation. There is however speed limitations.
  • Encryption / Signature
    • symmetrically. This library ships AES and Salsa20/20, but they are not exposed to end user. The symmetric cipher is constructed based upon these algorithms but there is more.
    • asymmetrically. There is an implementation within this library of asymmetric ciphers, that is not compatible with any existing standards. These ciphers utilize ECDH and ECDSA. Beginning from the basic seed secret, it derives secrets for initializing ECDH and ECDSA. Notice that the ECDH are primarily used for exchanging keys. In our library, the public key used in ECDH is the public key, the peer key is attached to the ciphertext for constructing shared secrets, which is laterly used for encrypting the actually payload.
  • enigma system
    • A data structure of identity key. Identity key is constructed with a subject within 256 characters, a public key and a self-signature on the subject and the public key.
    • A message data structure. A message data structure is either a payload, or an envelope carrying an encrypted payload and information for decryption.
      • Payload carries the signer, signature and the plaintext.
      • Envelope carries the encrypted payload, the decryptor(fingerprint), and information for the decryptor(s) to recover the key, and a sign of compression.
    • An abstract implementation of the enigma system
      • Functionalities are called via API
      • Functionalities are called using Q&A, parameters are asked, and exceptions are thrown.
      • The user uses only a few logic to forward the question to the user, or answer them by themselve, and the task is completed.
      • The storage, based on interfaces the same of localStorage, are managed by the implementation. The user needs only to maintain a localStorage.
  • Acceleration modules
    • based upon platforms running this library, these modules replaces some native features with interfaces provided by the platform.
    • for example, use the crypto library from NodeJS to hash, or use the interface on a customized browser to do random numbers more securely.
  • Utilities
    • convert between JavaScript Strings, ASCII Strings、Base32、Base64 and HEX.
    • operate on ArrayBuffer, to concat, do XOR, test if equal, and reverse.
    • generate random bytes.
    • generate UUID.
    • test variable types.
    • (not complete) LZW compression.
    • serialize and unserialize JSON with predefined data structures.

1 Basic knowledgements

1.1 Initializing the neoatlantis-crypto-js library

To initialize the library in NodeJS, use:

var crypto = require('./lib/neoatlantis-crypto-js.js'); // path to the library

Use RequireJS in browsers:

require(['lib/neoatlantis-crypto-js'], function(crypto){...}); // path to the library

1.2 Remarks

Most of the time, the IOs are in format of ArrayBuffer.

To read its content, use code like followings:

var result = // the ArrayBuffer you got
var ary = new Uint8Array(result);
ary.length; // length of the result
ary[0]; // get the byte at position 0, in range of [0, 255]

To construct an ArrayBuffer

var ary = new Uint8Array([1,2,3,4,4,3,2,1]);
ary.buffer; // ArrayBuffer

2 Use the utilities

Utilities as functions are provided under crypto.util. They are also used by our library itself.

2.1 Obtain random data

The most common variant is obtaining random bytes of length n:

var x = new crypto.util.srand().bytes(1024);
// x is the ArrayBuffer containing 1024 random bytes

var y = new crypto.util.srand().bytes(1024);
// y is of type Uint8Array, can be accessed with indexes.

You may use following code to make the data more random.

var rand = new crypto.util.srand();
rand.touch();

Using touch function, you affect the internal state of the random generator globally, although you have used new to generate rand. The random data are more unpredictable after that. You can bind this event to user inputs like mouse moves and keyboard types. The time of calling touch affects the random generator, not the type of your choosen input.

2.2 Dealing with ArrayBuffer

Use crypto.util.buffer, and process the ArrayBuffer type of data.

XOR. To xor 2 ArrayBuffers with same byteLength, use following code:

var buffer1 = ...
var buffer2 = ... // buffer1, buffer2 都是 ArrayBuffer
var output = crypto.util.buffer.xor(buffer1, buffer2);
// buffer1, buffer2的byteLength属性必须一样。output也是一个ArrayBuffer
// 如果buffer1.byteLength != buffer2.byteLength,将会抛出异常

Concatenation. To get a comprised ArrayBuffer, use following code:

var list = [buffer1, buffer2, buffer3, ..., bufferx]; // list[i] is ArrayBuffer
var output = crypto.util.buffer.concat(list); // get a new ArrayBuffer

If equal. To see if 2 ArrayBuffer are equal in content, use following code:

var buffer1 = ...
var buffer2 = ... // buffer1, buffer2 are ArrayBuffers
var output = crypto.util.buffer.equal(buffer1, buffer2);
// Only when buffer1 and buffer2 are ArrayBuffer and contains the same data,
// a true will be returned. Otherwise, return false.

Reverse. Swap the first item with the last item, the second with the one before last, etc.

var output = crypto.util.buffer.reverse(buffer); // buffer and output are ArrayBuffers

2.3 Test variable type

Use following code, to test if a variable is of a given type:

var variable = ...
var typeTest = crypto.util.type(variable);
// typeTest.isArrayBuffer() see if is an ArrayBuffer

// choices are:
// isError(), isArray(), isObject(), isPrimitive(), isFunction(), isDate()
// isNumber(), isString(), isBoolean(), isArrayBuffer()

2.4 Encoding conversion

Use following code, to get a encoded output, with an input of a string or ArrayBuffer.

var converter = crypto.util.encoding(inputVariable, 'ENCODING');
// inputVariable is a string or an ArrayBuffer

// ENCODING is used when inputVariable is a string. Possible values are: hex, ascii, base64, base32
// This instructs how the program will understand the string, and read it into buffered bytes.
// When not specified, it will be taken by default as normal strings(with no special encoding).
// ascii means, the input string contains only characters in ASCII.
// hex means, the input string has an odd length, comprised only with 0-9 and a-f.

converter.toArrayBuffer(); // get an ArrayBuffer as output.
// Or you may use: toHEX(), toUTF16(), toBase64(), toBase32(), toASCII(), toArray()

2.5 UUID generation

Use following code, to get an UUID out of crypto.util.srand.

var uuid = crypto.util.uuid(); // a string, something like 7c21b2c8-6370-13e4-bef9-3c280e55a5cf

2.6 Serialization and De-serialization

This library contains a serialization tool dealing with structured data. You may define a template of a structure, and fill with data, to get the serialized output, or reverse.

The input to be serialized is an Object in javascript. In the serialized result, keys are not included. That is to say, you have to know previously, which template may be used to deserialize the data.

You have following choices to define a data structure:

  • binary, byte streams. shortBinary, binary, longBinary are supported.
    • shortBinary takes one byte to save the length of stream, saves at most 255 bytes, and can be null.
    • binary takes 2 bytes, saves at most 65535 bytes, allows null.
    • longBinary takes 4 bytes, saves at most 2^32-1 bytes(about 4GB), can be null.
  • boolean, a boolean variable, takes one byte.
  • constant, sets a given ArrayBuffer into the serialized result. Takes the same length of constant in bytes. In deserialization, it will be asserted that a same field exists in the input, or an exception will be thrown.
  • enum, takes a selection of a predefined list. The list contains at most 255 items. Takes one byte in serialized result. During deserialization, the result will be translated into one string in the enumerate list.
  • datetime, takes a Date object as input. The resolution is one second. Takes 7 bytes in the result. Can record years from 0 to 65535.
  • array, an array of binaries. Supports shortArray, array, respectively 255 and 65535 items. CAUTION: shortArray stores only shortBinary, and array stores only binary type.
2.6.1 Define a template for serialization

Use following code to define a serializing template:

// see enigma/identity.js
var template = {
    '_': ['constant', new Uint8Array([69, 73]).buffer],
    'subject': 'shortBinary',
    'algorithm': ['enum',
        'NECRAC96',
        'NECRAC112',
        'NECRAC128',
        'NECRAC192',
        'NECRAC256',
    ],
    'public': 'binary',
    'secret': 'shortBinary',
    'signature': 'binary',
};

template is a javascript Object. In the example, _, subject, algorithm, public, secret, signature are defined. When this template is used in generating result, use such keys. But the key names are excluded from final result.

Notice that algorithm is a enum type. _ is a constant. Therefore when deserializing, it will be asserted that at _ a defined value could be read. This is used to distinguish an expected data structure at first glance.

2.6.2 To serialize a piece of data

Use following code to serialize:

var serializer = crypto.util.serialize(template); // use template to construct a tool

// fill data. The constant `_` doesn't have to be inputed.
var data = {
    'subject': new Uint8Array([...]).buffer,
    'algorithm': 'NECRAC96',
    'public': new Uint8Array([...]).buffer,
    'secret': new Uint8Array([...]).buffer,
    'signature': new Uint8Array([...]).buffer,
};

// get result
var result = serializer.serialize(data); // result is an ArrayBuffer
2.6.3 To deserialize a piece of data

Use following code, to deserialize from a given ArrayBuffer:

var deserializer = crypto.util.serialize(template); // use template to construct a deserializer

// get result
var result = deserializer.deserialize(data); // data should be an ArrayBuffer

Remember to handle possible exceptions during deserialization with try-catch. This happens when the data doesn’t match a given template.

3 Hashing

The library provides an interface for doing hashes on data, for calculating MAC(Message Authentication Code), and for doing a slow PBKDF2. The internal implementation is done with Whirlpool algorithm, the output length can be configured from 1 byte to 64 bytes.

3.1 Calculate hashes of an ArrayBuffer

Use following code to calculate hashes of an ArrayBuffer:

var ary = ... // ArrayBuffer
var hasher = new crypto.hash(OUTPUTLENGTH); // Select the output length in bytes

var result = hasher.hash(ary);
result.buffer; // obtains an `ArrayBuffer`
result.hex; // obtains a string representing the result with HEX(0-9, a-f).

3.2 Calculate MAC of an ArrayBuffer

MAC(Message Authentication Code) is another hash with an external key input. You may use it to authenticate the integrity of a message against a known key.

var ary = ... // ArrayBuffer
var key = ... // ArrayBuffer

var hasher = new crypto.hash(32); // specify the output length to 32 bytes
var result = hasher.mac(ary, key);
result.buffer; // got an `ArrayBuffer`
result.hex; // got a string represented with HEX characters(0-9, a-f).

3.3 PBKDF2 key derivation

PBKDF2 key derivation consumes computing resources, and can make a stronger key basing on a weaker password(from user input, for example). This may be useful against brute forces, or to improve the quality(randomness) of the key.

var key = ... // ArrayBuffer
var salt = ... // ArrayBuffer
var iterations = 10000; // Larger is better, but takes more time
var len = 30; // Bytes of result

var hasher = new crypto.hash();
var result = hasher.pbkdf2(key, salt, iterations, len);
result.buffer; // got an `ArrayBuffer`
result.hex; // got a string represented with HEX characters(0-9, a-f)

PBKDF2 algorithms calls internally MAC algorithms, as specified in 3.2. In such calls, the MAC algorithm are not configured to slice its results, they are called with full bytes output.

4 Symmetric Encryptions

This library provides a single symmetric encryption interface. By entering the key, the data can be encrypted or decrypted. ArrayBuffer of any length can be treated.

The symmetric encryption algorithm contains following steps. Take encryption as an example:

  1. choose a 10-bit salt
  2. derive intermediate keys from key at input for all actual algorithms
  3. use hash algorithms to calculate the MAC of plaintext(against a key produced in previous step) with a length of 6 bytes. It will be attached to plaintext at beginning.
  4. pad the result plaintext into a length which is the multiple of 16 bytes(RFC5652). Then encrypt it first using Salsa20/20, then AES-128-ECB.

Since Salsa20/20 is used, using AES-128 in ECB mode will not introduce its security flaws. MAC is used to validate if a successful decryption.

To use symmetric encryptions:

var encryptor = new crypto.cipher.symmetric();
encryptor.key(key); // key is ArrayBuffer
encryptor.encrypt(data); // data is ArrayBuffer, returning results of ArrayBuffer

To decrypt:

var decryptor = new crypto.cipher.symmetric();
decryptor.key(key); // key is ArrayBuffer
decryptor.decrypt(data); // data is ArrayBuffer, returning results of ArrayBuffer

5 Asymmetric Encryptions and Signatures

The asymmetric algorithms provides by this library are in suites. Their implementations and output formats are not obeying any existing standards.

Each suite of algorithm has a name. By specifying this name, you are specifying the actual algorithms for encryptions and signatures(as well as their parameters), and the hash algorithm before doing a signature. Thanks to this definition, each algorithm can be used to both encryption and signature.

And, as by design to ease the secret key management, the secret key of a user is always some random bytes that can be assigned outside the library. By specifying the secret key and the algorithm, the public key can be derived.

Currently supported suites of algorithms are listed as follows:

Name Encrypt Algorithm Sign Algorithm Hash Algorithm Private Key Length(bit)
NECRAC256 ECDH/secp521r1 ECDSA/secp521r1 WHIRLPOOL 64 x 8 = 512
NECRAC192 ECDH/secp384r1 ECDSA/secp384r1 BLAKE2s 48 x 8 = 384
NECRAC128 ECDH/secp256k1 ECDSA/secp256r1 BLAKE2s 32 x 8 = 256
NECRAC112 ECDH/secp224r1 ECDSA/secp224r1 RIPEMD160 28 x 8 = 224
NECRAC96 ECDH/secp192r1 ECDSA/secp192k1 RIPEMD160 24 x 8 = 192

By selecting any suite and initializing the asymmetric cipher instance, random inputs can be proceeded, aka be encrypted/decrypted/signed/verified.

5.1 Initializing an asymmetric cipher instance

使用如下代码,根据选取的算法套装,初始化一个不对称加密的对象。

var algorithm = 'NECRAC128'; // 见上文列出的算法名
var asym = crypto.cipher.asymmetric(algorithm);

在初始化之后,asym有2个方法可供进一步调用,即setPrivateKeysetPublicKeysetPrivateKey用来设定一个私钥。其参数是被随机的字节填充的ArrayBuffer类型的数据,长度由上文表格确定。 setPublicKey用来设定一个公钥,由下文所述的导出公钥的方式获取,其变量类型也是ArrayBuffer

调用这2个方法之一后,asym将会暴露其他的方法以供进一步操作。 同时,asym将删去已经暴露的setPrivateKeysetPublicKey方法,禁止重新进行初始化操作。

5.2 使用不对称加密对象

设定了公钥或者私钥之后的asym对象,可以被用来进行如下操作:

5.2.1 推导公钥

在指定了一个任意的私钥之后,使用算法推导给出对应于这个私钥的公钥。 之后这个公钥就可以被公开,以便让他人藉此向私钥持有者加密发送消息,或者公钥持有者验证私钥持有者签署过的消息。

var publicKeyBuf = asym.getPublicKey(); // 返回 ArrayBuffer

为此,asym对象必须是通过setPrivateKey方法初始化的。

5.2.2 加密

使用加密方法,可以在得到别人的公钥之后,向他发送加密的数据。只有持有和公钥对应的私钥的人才能解密数据,其他人不能。

var plaintextBuf = ... // ArrayBuffer
var ciphertextBuf = asym.encrypt(plaintextBuf); // 返回 ArrayBuffer

使用setPrivateKeysetPublicKey方法通过输入私钥或者公钥初始化的asym对象都可以调用这一功能。

5.2.3 解密

已知一个私钥,解密别人发来的数据。

var plaintextBuf2 = asym.decrypt(ciphertextBuf); // 返回 ArrayBuffer

只有使用setPrivateKey方法通过私钥初始化的asym对象可以调用这一功能。

5.2.4 签名

签名的功能是对给定的plaintextBuf输入,生成一段数据,其他人拥有公钥的时候,可以据此确定确实是私钥的拥有者进行了“签名”这一操作,而不是任何其他人。 此外,签名者自己不能否定自己曾经签署过这段数据。

var signatureBuf = asym.sign(plaintextBuf); // 返回 ArrayBuffer

只有使用setPrivateKey方法通过私钥初始化的asym对象可以调用这一功能。

5.2.5 验证签名

在得到别人的公钥之后,可以验证此人所签署过的数据。需要输入声称被此人签署过的数据plaintextBuf2和签名数据signatureBuf2

var result = asym.verify(plaintextBuf2, signatureBuf2); // 返回 true 或者 false

使用setPrivateKeysetPublicKey方法通过输入私钥或者公钥初始化的asym对象都可以调用这一功能。 但是自然,一般在实际应用中,有意义的用法是通过输入公钥进行的初始化。

6 Enigma