├── .gitignore ├── .github └── FUNDING.yml ├── lib ├── error │ ├── CryptoError.js │ └── FilesystemError.js ├── key │ ├── SymmetricKey.js │ ├── AsymmetricPublicKey.js │ └── AsymmetricSecretKey.js ├── AsymmetricFile.js ├── SymmetricFile.js ├── Util.js ├── Password.js ├── Symmetric.js ├── Asymmetric.js └── Keyring.js ├── .travis.yml ├── test ├── async-test-helper.js ├── keys-test.js ├── asymmetricfile-test.js ├── password-test.js ├── symmetricfile-test.js ├── util-test.js ├── symmetric-test.js ├── keyring-test.js ├── asymmetric-test.js └── test-vectors.json ├── index.js ├── LICENSE.txt ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /node_modules 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | patreon: soatok 4 | ko_fi: soatok 5 | custom: ['https://paypal.me/soatok'] 6 | -------------------------------------------------------------------------------- /lib/error/CryptoError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class CryptoError 3 | * @package dholecrypto 4 | */ 5 | class CryptoError extends Error 6 | { 7 | 8 | } 9 | 10 | module.exports = CryptoError; 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "14" 4 | - "13" 5 | - "12" 6 | - "11" 7 | 8 | script: 9 | - npm test 10 | - npm install --save sodium-native 11 | - npm test 12 | -------------------------------------------------------------------------------- /lib/error/FilesystemError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class FilesystemError 3 | * @package dholecrypto 4 | */ 5 | class FilesystemError extends Error 6 | { 7 | 8 | } 9 | 10 | module.exports = FilesystemError; 11 | -------------------------------------------------------------------------------- /test/async-test-helper.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | module.exports = async function expectError(promised, message) { 3 | let thrown = false; 4 | try { 5 | await promised; 6 | } catch (e) { 7 | thrown = true; 8 | expect(message).to.be.equal(e.message); 9 | } 10 | if (!thrown) { 11 | throw new Error('Function did not throw as expected.'); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | 'Asymmetric': require('./lib/Asymmetric'), 4 | 'AsymmetricFile': require('./lib/AsymmetricFile'), 5 | 'AsymmetricSecretKey': require('./lib/key/AsymmetricSecretKey'), 6 | 'AsymmetricPublicKey': require('./lib/key/AsymmetricPublicKey'), 7 | 'CryptoError': require('./lib/error/CryptoError'), 8 | 'DholeUtil': require('./lib/Util'), 9 | 'Keyring': require('./lib/Keyring'), 10 | 'Password': require('./lib/Password'), 11 | 'Symmetric': require('./lib/Symmetric'), 12 | 'SymmetricFile': require('./lib/SymmetricFile'), 13 | 'SymmetricKey': require('./lib/key/SymmetricKey') 14 | }; 15 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | ISC License (ISC) 2 | 3 | Copyright 2019 Soatok Dreamseeker 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose 6 | with or without fee is hereby granted, provided that the above copyright notice 7 | and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 11 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 13 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 14 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 15 | THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /lib/key/SymmetricKey.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const CryptoError = require('../error/CryptoError'); 4 | const { SodiumPlus, CryptographyKey } = require('sodium-plus'); 5 | const Util = require('../Util'); 6 | let sodium; 7 | 8 | /** 9 | * @class SymmetricKey 10 | * @package dholecrypto.key 11 | */ 12 | module.exports = class SymmetricKey extends CryptographyKey 13 | { 14 | constructor(stringOrBuffer) { 15 | super(Util.stringToBuffer(stringOrBuffer)); 16 | if (this.buffer.length !== 32) { 17 | throw new CryptoError( 18 | `Symmetric keys must be 32 bytes. ${this.buffer.length} given.` 19 | ); 20 | } 21 | } 22 | 23 | /** 24 | * @return {SymmetricKey} 25 | */ 26 | static async generate() { 27 | if (!sodium) sodium = await SodiumPlus.auto(); 28 | return new SymmetricKey( 29 | await sodium.randombytes_buf(32) 30 | ); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dhole-crypto", 3 | "version": "0.7.2", 4 | "description": "JavaScript port of soatok/dhole-cryptography from PHP", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "repository": "https://github.com/soatok/dholecrypto-js", 8 | "keywords": [ 9 | "argon2id", 10 | "ed25519", 11 | "blake2b", 12 | "hmac-sha512256", 13 | "poly1305", 14 | "x25519", 15 | "xchacha20", 16 | "cryptography", 17 | "crypto", 18 | "libsodium", 19 | "sodium", 20 | "dhole", 21 | "dholes", 22 | "furry", 23 | "furries", 24 | "furry fandom" 25 | ], 26 | "author": "Soatok Dreamseeker", 27 | "license": "ISC", 28 | "directories": { 29 | "test": "test" 30 | }, 31 | "scripts": { 32 | "test": "mocha" 33 | }, 34 | "dependencies": { 35 | "assert": "^2.0.0", 36 | "load-json-file": "^6.2.0", 37 | "rfc4648": "^1.4.0", 38 | "sodium-plus": "^0.9.0", 39 | "typedarray-to-buffer": "^3.1.5" 40 | }, 41 | "devDependencies": { 42 | "chai": "^4.2.0", 43 | "mocha": "^8.2.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/key/AsymmetricPublicKey.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { SodiumPlus, Ed25519PublicKey, X25519PublicKey } = require('sodium-plus'); 4 | const Util = require('../Util'); 5 | let sodium; 6 | 7 | /** 8 | * @class AsymmetricPublicKey 9 | * @package dholecrypto.key 10 | */ 11 | module.exports = class AsymmetricPublicKey extends Ed25519PublicKey 12 | { 13 | constructor(stringOrBuffer) { 14 | super(Util.stringToBuffer(stringOrBuffer)); 15 | } 16 | 17 | /** 18 | * Get a birationally equivalent X25519 secret key 19 | * for use in crypto_box_* 20 | * 21 | * @return {Buffer} length = 32 22 | */ 23 | async getBirationalPublic() { 24 | if (!sodium) sodium = await SodiumPlus.auto(); 25 | if (typeof this.birationalPublic === 'undefined') { 26 | this.birationalPublic = await sodium.crypto_sign_ed25519_pk_to_curve25519(this); 27 | } 28 | return this.birationalPublic; 29 | } 30 | 31 | injectBirationalEquivalent(buf) { 32 | this.birationalPublic = new X25519PublicKey(Util.stringToBuffer(buf)); 33 | return this; 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /test/keys-test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const expect = require('chai').expect; 3 | const AsymmetricSecretKey = require('../lib/key/AsymmetricSecretKey'); 4 | const AsymmetricPublicKey = require('../lib/key/AsymmetricPublicKey'); 5 | const SymmetricKey = require('../lib/key/SymmetricKey'); 6 | 7 | describe('Keys', function() { 8 | it('SymmetricKey', async function () { 9 | expect(() => { 10 | new SymmetricKey('x') 11 | }).to.throw('Symmetric keys must be 32 bytes. 1 given.'); 12 | }); 13 | it('AsymmetricSecretKey', async function () { 14 | expect(() => { 15 | new AsymmetricSecretKey('x') 16 | }).to.throw('Ed25519 secret keys must be 64 bytes long'); 17 | expect(() => { 18 | new AsymmetricSecretKey(Buffer.alloc(64), 'x') 19 | }).to.throw('Second argument must be an AsymmetricPublicKey'); 20 | new AsymmetricSecretKey(Buffer.alloc(64), null); 21 | }); 22 | it('AsymmetricPublicKey', async function () { 23 | expect(() => { 24 | new AsymmetricPublicKey('x') 25 | }).to.throw('Ed25519 public keys must be 32 bytes long'); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/asymmetricfile-test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const base32 = require('rfc4648').base32; 3 | const fs = require('fs'); 4 | const fsp = fs.promises; 5 | const AsymmetricFile = require('../lib/AsymmetricFile'); 6 | const AsymmetricSecretKey = require('../lib/key/AsymmetricSecretKey'); 7 | const Keyring = require('../lib/Keyring'); 8 | const Util = require('../lib/Util'); 9 | const loadJsonFile = require('load-json-file'); 10 | 11 | describe('AsymmetricFile', function() { 12 | it('sign()', async function() { 13 | let aliceSk = await AsymmetricSecretKey.generate(); 14 | let alicePk = aliceSk.getPublicKey(); 15 | 16 | let buffer = base32.stringify(Util.randomBytes(10000)); 17 | await fsp.writeFile(__dirname + "/signtest1.txt", buffer); 18 | let fh = await fsp.open(__dirname + "/signtest1.txt", 'r'); 19 | let sig = await AsymmetricFile.sign(fh, aliceSk); 20 | assert(await AsymmetricFile.verify(fh, alicePk, sig), 'Signatures not valid'); 21 | await fh.close(); 22 | await fsp.unlink(__dirname + "/signtest1.txt"); 23 | }); 24 | 25 | it('should pass the standard test vectors', async function() { 26 | let json = await loadJsonFile('./test/test-vectors.json'); 27 | let keyring = new Keyring(); 28 | let publicKey = await keyring.loadAsymmetricPublicKey( 29 | json['asymmetric-file-sign']['public-key'] 30 | ); 31 | let i = 2; 32 | let fh, filename; 33 | for (let test of json['asymmetric-file-sign'].tests) { 34 | filename = __dirname + "/signtest" + i + ".txt"; 35 | await fsp.writeFile(filename, test.contents); 36 | fh = await fsp.open(filename, 'r'); 37 | assert( 38 | await AsymmetricFile.verify(fh, publicKey, test.signature), 39 | 'Signatures not valid' 40 | ); 41 | await fh.close(); 42 | await fsp.unlink(filename); 43 | } 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/password-test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const expect = require('chai').expect; 3 | const Symmetric = require('../lib/Symmetric'); 4 | const Password = require('../lib/Password'); 5 | const SymmetricKey = require('../lib/key/SymmetricKey'); 6 | const Util = require('../lib/Util'); 7 | const base64url = require('rfc4648').base64url; 8 | const hex = require('rfc4648').base16; 9 | const loadJsonFile = require('load-json-file'); 10 | 11 | describe('Password', function() { 12 | it('should validate a message', async function() { 13 | this.timeout(0); 14 | let symKey = await SymmetricKey.generate(); 15 | let password = "Cowwect hoss battewy staple UwU"; 16 | let hasher = new Password(symKey); 17 | let pwhash = await hasher.hash(password); 18 | let verify = await hasher.verify(password, pwhash); 19 | expect(verify).to.be.equal(true); 20 | }); 21 | 22 | it('should pass the standard test vectors', async function() { 23 | this.timeout(0); 24 | let json = await loadJsonFile('./test/test-vectors.json'); 25 | let keys = {}; 26 | let hasher = {}; 27 | let k; 28 | for (k in json.symmetric.keys) { 29 | keys[k] = new SymmetricKey( 30 | Util.stringToBuffer(base64url.parse(json.symmetric.keys[k])) 31 | ); 32 | hasher[k] = new Password(keys[k]); 33 | } 34 | 35 | let key; 36 | let test; 37 | let check; 38 | for (let i = 0; i < json.password.valid.length; i++) { 39 | test = json.password.valid[i]; 40 | pwhash = hasher[test.key]; 41 | try { 42 | check = await pwhash.verify( 43 | test.password, 44 | test['encrypted-pwhash'], 45 | test.aad 46 | ); 47 | } catch (e) { 48 | console.log("Failure at index " + i); 49 | throw e; 50 | } 51 | expect(check).to.be.equal(true); 52 | } 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /lib/AsymmetricFile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const AsymmetricPublicKey = require('./key/AsymmetricPublicKey'); 4 | const AsymmetricSecretKey = require('./key/AsymmetricSecretKey'); 5 | const base64url = require('rfc4648').base64url; 6 | const SymmetricFile = require('./SymmetricFile'); 7 | const Util = require('./Util'); 8 | const { SodiumPlus } = require('sodium-plus'); 9 | let sodium; 10 | 11 | module.exports = class AsymmetricFile { 12 | /** 13 | * @param {string|FileHandle} file 14 | * @param {AsymmetricSecretKey} secretKey 15 | * @returns {Promise} 16 | */ 17 | static async sign(file, secretKey) { 18 | if (!sodium) sodium = await SodiumPlus.auto(); 19 | if (!(secretKey instanceof AsymmetricSecretKey)) { 20 | throw new TypeError("Argument 2 must be an instance of AsymmetricSecretKey."); 21 | } 22 | let entropy = await Util.randomBytes(32); 23 | let hash = await SymmetricFile.hash(file, entropy); 24 | let signature = await sodium.crypto_sign_detached(hash, secretKey); 25 | return base64url.stringify( 26 | Buffer.from( 27 | signature.toString('binary') + entropy.toString('binary'), 28 | 'binary' 29 | ) 30 | ); 31 | } 32 | 33 | /** 34 | * @param {string|Buffer} file 35 | * @param {AsymmetricPublicKey} pk 36 | * @param {string|Buffer} signature 37 | * @return {boolean} 38 | */ 39 | static async verify(file, pk, signature) { 40 | if (!sodium) sodium = await SodiumPlus.auto(); 41 | if (!(pk instanceof AsymmetricPublicKey)) { 42 | throw new TypeError("Argument 2 must be an instance of AsymmetricPublicKey."); 43 | } 44 | let decoded = Util.stringToBuffer(base64url.parse(signature)); 45 | let sig = decoded.slice(0, 64); 46 | let entropy = decoded.slice(64, 96); 47 | let hash = await SymmetricFile.hash(file, entropy); 48 | return sodium.crypto_sign_verify_detached( 49 | hash, 50 | pk, 51 | sig 52 | ); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /lib/SymmetricFile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require('fs'); 4 | const fsp = fs.promises; 5 | const Util = require('./Util'); 6 | const { SodiumPlus } = require('sodium-plus'); 7 | let sodium; 8 | 9 | const BUFFER_SIZE = 8192; 10 | 11 | module.exports = class SymmetricFile { 12 | /** 13 | * @param {string|FileHandle} file 14 | * @param {string|Buffer} preamble 15 | * @returns {Promise} 16 | */ 17 | static async hash(file, preamble = '') { 18 | if (!sodium) sodium = await SodiumPlus.auto(); 19 | if (typeof (file) === 'string') { 20 | let handle = await fsp.open(file, 'r'); 21 | try { 22 | return await SymmetricFile.hashFileHandle( 23 | handle, 24 | preamble 25 | ); 26 | } finally { 27 | handle.close(); 28 | } 29 | } 30 | /* istanbul ignore if */ 31 | if (typeof(file) === 'number') { 32 | throw new TypeError('File must be a file handle or a path'); 33 | } 34 | return await SymmetricFile.hashFileHandle(file, preamble); 35 | } 36 | 37 | /** 38 | * 39 | * @param {FileHandle} fh 40 | * @param {string|Buffer} preamble 41 | * @returns {Promise} 42 | */ 43 | static async hashFileHandle(fh, preamble = '') { 44 | if (!sodium) sodium = await SodiumPlus.auto(); 45 | let stat = await fh.stat(); 46 | let buf = Buffer.alloc(BUFFER_SIZE); 47 | let state = await sodium.crypto_generichash_init(null, 64); 48 | let prefix = Util.stringToBuffer(preamble); 49 | if (prefix.length > 0) { 50 | await sodium.crypto_generichash_update(state, prefix); 51 | } 52 | 53 | let start = 0; 54 | let toRead = 0; 55 | while ( start < stat.size) { 56 | toRead = Math.min((stat.size - start), BUFFER_SIZE); 57 | await fh.read(buf, 0, toRead, start); 58 | await sodium.crypto_generichash_update(state, buf.slice(0, toRead)); 59 | start += toRead; 60 | } 61 | let output = await sodium.crypto_generichash_final(state, 64); 62 | return Buffer.concat([prefix, output]); 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /lib/key/AsymmetricSecretKey.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { SodiumPlus, Ed25519SecretKey, X25519SecretKey } = require('sodium-plus'); 4 | const AsymmetricPublicKey = require('./AsymmetricPublicKey'); 5 | const Util = require('../Util'); 6 | let sodium; 7 | 8 | /** 9 | * @class AsymmetricSecretKey 10 | * @package dholecrypto.key 11 | */ 12 | module.exports = class AsymmetricSecretKey extends Ed25519SecretKey 13 | { 14 | constructor(stringOrBuffer) { 15 | super(Util.stringToBuffer(stringOrBuffer)); 16 | if (arguments.length > 1) { 17 | if (arguments[1] instanceof AsymmetricPublicKey) { 18 | this.pk = arguments[1]; 19 | } else if (typeof arguments[1] === 'null' || arguments[1] === null) { 20 | this.pk = new AsymmetricPublicKey(this.buffer.slice(32, 64)); 21 | } else { 22 | throw new TypeError("Second argument must be an AsymmetricPublicKey"); 23 | } 24 | } else { 25 | this.pk = new AsymmetricPublicKey(this.buffer.slice(32, 64)); 26 | } 27 | } 28 | 29 | /** 30 | * @return {AsymmetricSecretKey} 31 | */ 32 | static async generate() { 33 | if (!sodium) sodium = await SodiumPlus.auto(); 34 | let keypair = await sodium.crypto_sign_keypair(); 35 | return new AsymmetricSecretKey( 36 | keypair.slice(0, 64), 37 | new AsymmetricPublicKey(keypair.slice(64, 96)) 38 | ); 39 | } 40 | 41 | /** 42 | * Get a birationally equivalent X25519 secret key 43 | * for use in crypto_box_* 44 | * 45 | * @return {Buffer} length = 32 46 | */ 47 | async getBirationalSecret() { 48 | if (!sodium) sodium = await SodiumPlus.auto(); 49 | if (typeof this.birationalSecret === 'undefined') { 50 | this.birationalSecret = await sodium.crypto_sign_ed25519_sk_to_curve25519(this); 51 | } 52 | return this.birationalSecret; 53 | } 54 | 55 | /** 56 | * @return {AsymmetricPublicKey} 57 | */ 58 | getPublicKey() { 59 | return this.pk; 60 | } 61 | 62 | /** 63 | * @param {Buffer} buf 64 | * @returns {AsymmetricSecretKey} 65 | */ 66 | injectBirationalEquivalent(buf) { 67 | this.birationalSecret = new X25519SecretKey(Util.stringToBuffer(buf)); 68 | return this; 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /test/symmetricfile-test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const base32 = require('rfc4648').base32; 3 | const base64url = require('rfc4648').base64url; 4 | const {expect} = require('chai'); 5 | const fs = require('fs'); 6 | const fsp = fs.promises; 7 | const hex = require('rfc4648').base16; 8 | const loadJsonFile = require('load-json-file'); 9 | const SymmetricFile = require('../lib/SymmetricFile'); 10 | const Util = require('../lib/Util'); 11 | const { SodiumPlus } = require('sodium-plus'); 12 | let sodium; 13 | 14 | describe('SymmetricFile', function() { 15 | it('hash()', async function() { 16 | if (!sodium) sodium = await SodiumPlus.auto(); 17 | let buf; 18 | let i = 1; 19 | let file; 20 | let a, b; 21 | let random; 22 | for (let len of [32, 64, 100, 1000, 10000]) { 23 | buf = base32.stringify(await Util.randomBytes(len)); 24 | await fsp.writeFile(__dirname + "/test" + i + ".txt", buf); 25 | file = await fsp.open(__dirname + "/test" + i + ".txt", 'r'); 26 | 27 | // First test case... 28 | a = await SymmetricFile.hash(file); 29 | b = await sodium.crypto_generichash(Util.stringToBuffer(buf), null, 64); 30 | expect(hex.stringify(a)).to.be.equal(hex.stringify(b)); 31 | 32 | // Second test case... 33 | random = await Util.randomBytes(32); 34 | a = await SymmetricFile.hash(file, random); 35 | b = await sodium.crypto_generichash( 36 | Buffer.concat([random, Util.stringToBuffer(buf)]), 37 | null, 38 | 64 39 | ); 40 | expect( 41 | hex.stringify(a) 42 | ).to.be.equal( 43 | hex.stringify( 44 | Buffer.concat([random, b]) 45 | ) 46 | ); 47 | await file.close(); 48 | await fsp.unlink(__dirname + "/test" + i + ".txt"); 49 | i++; 50 | } 51 | i++; 52 | buf = base32.stringify(await Util.randomBytes(32)); 53 | let finExpect = await sodium.crypto_generichash(Util.stringToBuffer(buf), null, 64); 54 | await fsp.writeFile(__dirname + "/test" + i + ".txt", buf); 55 | let finHash = await SymmetricFile.hash(__dirname + "/test" + i + ".txt"); 56 | await fsp.unlink(__dirname + "/test" + i + ".txt"); 57 | expect(hex.stringify(finHash)).to.be.equal(hex.stringify(finExpect)); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/util-test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const expect = require('chai').expect; 3 | const Util = require('../lib/Util'); 4 | const hex = require('rfc4648').base16; 5 | 6 | describe('Util', function () { 7 | it('hashEquals()', async function () { 8 | let a = await Util.randomBytes(32); 9 | let b = await Util.randomBytes(32); 10 | let c = await Util.randomBytes(31); 11 | expect(true).to.be.equal(await Util.hashEquals(a, a)); 12 | expect(true).to.be.equal(await Util.hashEquals(b, b)); 13 | expect(false).to.be.equal(await Util.hashEquals(a, b)); 14 | expect(false).to.be.equal(await Util.hashEquals(b, a)); 15 | expect(false).to.be.equal(await Util.hashEquals(a, c)); 16 | }); 17 | 18 | it('randomBytes() uniqueness', async function () { 19 | let a = await Util.randomBytes(16); 20 | let b = await Util.randomBytes(16); 21 | expect(a.toString('hex')).to.not.equals(b.toString('hex')); 22 | }); 23 | 24 | it ('randomInt() uniqueness', async function () { 25 | let a, b; 26 | for (let i = 0; i < 1000; i++) { 27 | a = await Util.randomInt(0, Number.MAX_SAFE_INTEGER); 28 | b = await Util.randomInt(0, Number.MAX_SAFE_INTEGER); 29 | expect(a).to.not.equals(b); 30 | } 31 | }); 32 | 33 | it ('randomInt() distribution', async function () { 34 | let space = [0,0,0,0,0]; 35 | let iter = 0; 36 | let inc; 37 | let i; 38 | let failureSpotted; 39 | while (iter < 10000) { 40 | inc = await Util.randomInt(0, space.length - 1); 41 | space[inc]++; 42 | failureSpotted = false; 43 | for (i = 0; i < space.length; i++) { 44 | if (space[i] < 10) { 45 | failureSpotted = true; 46 | break; 47 | } 48 | } 49 | if (!failureSpotted) { 50 | break; 51 | } 52 | iter++; 53 | } 54 | expect(failureSpotted).to.be.equal(false); 55 | expect(iter).to.not.equals(10000); 56 | expect(await Util.randomInt(10, 4)).to.be.equal(10); 57 | }); 58 | 59 | it('stringToBuffer()', async function () { 60 | let buf = await Util.stringToBuffer('abc'); 61 | expect('616263').to.be.equal(buf.toString('hex')); 62 | buf = await Util.stringToBuffer(Buffer.from([0x41, 0x42, 0x43])); 63 | expect('414243').to.be.equal(buf.toString('hex')); 64 | buf = await Util.stringToBuffer(new Uint8Array([0x41, 0x42, 0x43])); 65 | expect('414243').to.be.equal(buf.toString('hex')); 66 | expect(() => { 67 | Util.stringToBuffer(12345) 68 | }).to.throw('Invalid type; string or buffer expected'); 69 | }); 70 | }); -------------------------------------------------------------------------------- /lib/Util.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const crypto = require('crypto'); 4 | const toBuffer = require('typedarray-to-buffer'); 5 | const { SodiumPlus } = require('sodium-plus'); 6 | let sodium; 7 | 8 | /** 9 | * @class Util 10 | * @package dholecrypto 11 | */ 12 | module.exports = class Util 13 | { 14 | /** 15 | * Generate a sequence of random bytes. 16 | * 17 | * @param {Number} amount 18 | * @returns {Buffer} 19 | */ 20 | static async randomBytes(amount) { 21 | if (!sodium) sodium = await SodiumPlus.auto(); 22 | return sodium.randombytes_buf(amount); 23 | } 24 | 25 | /** 26 | * Generate a random integer 27 | * 28 | * @param {Number} min 29 | * @param {Number} max 30 | * @returns {Number} 31 | */ 32 | static async randomInt(min = 0, max = 65535) { 33 | let i = 0, rval = 0, bits = 0, bytes = 0; 34 | let range = max - min; 35 | /* istanbul ignore if */ 36 | if (max > min && range < 0) { 37 | throw new Error('Integer overflow in range calculation'); 38 | } 39 | if (range < 1) { 40 | return min; 41 | } 42 | // Calculate Math.ceil(Math.log(range, 2)) using binary operators 43 | let tmp = range; 44 | /** 45 | * mask is a binary string of 1s that we can & (binary AND) with our random 46 | * value to reduce the number of lookups 47 | */ 48 | let mask = 1; 49 | while (tmp > 0) { 50 | if (bits % 8 === 0) { 51 | bytes++; 52 | } 53 | bits++; 54 | mask = mask << 1 | 1; // 0x00001111 -> 0x00011111 55 | tmp = tmp >>> 1; // 0x01000000 -> 0x00100000 56 | } 57 | 58 | let values; 59 | do { 60 | values = await this.randomBytes(bytes); 61 | 62 | // Turn the random bytes into an integer 63 | rval = 0; 64 | for (i = 0; i < bytes; i++) { 65 | rval |= (values[i] << (8 * i)); 66 | } 67 | // Apply the mask 68 | rval &= mask; 69 | // We discard random values outside of the range and try again 70 | // rather than reducing by a modulo to avoid introducing bias 71 | // to our random numbers. 72 | } while (rval > range); 73 | 74 | // We should return a value in the interval [min, max] 75 | return (rval + min); 76 | } 77 | 78 | /** 79 | * Coerce input to a Buffer, throwing a TypeError if it cannot be coerced. 80 | * 81 | * @param {string|Buffer|Uint8Array} stringOrBuffer 82 | * @returns Buffer 83 | */ 84 | static stringToBuffer(stringOrBuffer) { 85 | if (Buffer.isBuffer(stringOrBuffer)) { 86 | return stringOrBuffer; 87 | } else if (typeof(stringOrBuffer) === 'string') { 88 | return Buffer.from(stringOrBuffer, 'binary'); 89 | } else if (stringOrBuffer instanceof Uint8Array) { 90 | return toBuffer(stringOrBuffer); 91 | } else { 92 | throw new TypeError("Invalid type; string or buffer expected"); 93 | } 94 | } 95 | 96 | /** 97 | * Compare two strings without timing leaks. 98 | * 99 | * @param {string|Buffer} a 100 | * @param {string|Buffer} b 101 | * @returns {boolean} 102 | */ 103 | static hashEquals(a, b) { 104 | if (a.length !== b.length) { 105 | return false; 106 | } 107 | return crypto.timingSafeEqual( 108 | Util.stringToBuffer(a), 109 | Util.stringToBuffer(b) 110 | ); 111 | } 112 | }; 113 | -------------------------------------------------------------------------------- /lib/Password.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const base64url = require('rfc4648').base64url; 4 | const Util = require('./Util'); 5 | const CryptoError = require('./error/CryptoError'); 6 | const SymmetricKey = require('./key/SymmetricKey'); 7 | const Symmetric = require('./Symmetric'); 8 | const { SodiumPlus } = require('sodium-plus'); 9 | let sodium; 10 | 11 | /** 12 | * @name Symmetric 13 | * @package dholecrypto 14 | */ 15 | module.exports = class Password { 16 | /** 17 | * @param {SymmetricKey} symmetricKey 18 | * @param {object} options 19 | */ 20 | constructor(symmetricKey, options = null) { 21 | if (!(symmetricKey instanceof SymmetricKey)) { 22 | throw new TypeError("Argument 1 must be an instance of SymmetricKey"); 23 | } 24 | this.symmetricKey = symmetricKey; 25 | let defaultOpts = { 26 | "alg": "argon2id", 27 | "mem": 1 << 26, 28 | "ops": 2 29 | }; 30 | this.options = defaultOpts; 31 | if (typeof options === 'object') { 32 | if (options !== null) { 33 | this.options['mem'] = options.mem || defaultOpts.mem; 34 | this.options['ops'] = options.ops || defaultOpts.ops; 35 | } 36 | } 37 | } 38 | 39 | /** 40 | * @param {string|Buffer} password 41 | * @param {string|Buffer} ad 42 | * @return {string} 43 | */ 44 | async hash(password, ad = '') { 45 | if (!sodium) sodium = await SodiumPlus.auto(); 46 | let pwhash = await sodium.crypto_pwhash_str( 47 | password, 48 | this.options['ops'], 49 | this.options['mem'] 50 | ); 51 | if (ad.length > 0) { 52 | return Symmetric.encryptWithAd(pwhash, this.symmetricKey, ad); 53 | } 54 | return Symmetric.encrypt(pwhash, this.symmetricKey); 55 | } 56 | 57 | /** 58 | * @param {string|Buffer} pwhash 59 | * @param {string|Buffer} ad 60 | * @return {boolean} 61 | */ 62 | async needsRehash(pwhash, ad = '') { 63 | if (!sodium) sodium = await SodiumPlus.auto(); 64 | let decrypted; 65 | let encoded = `m=${this.options.mem >> 10},t=${this.options.ops},p=1`; 66 | if (ad.length > 0) { 67 | decrypted = await Symmetric.decryptWithAd(pwhash, this.symmetricKey, ad); 68 | } else { 69 | decrypted = await Symmetric.decrypt(pwhash, this.symmetricKey); 70 | } 71 | 72 | // $argon2id$v=19$m=65536,t=2,p=1$salt$hash 73 | // \######/ \#############/ 74 | // \####/ \###########/ 75 | // `--' `---------' 76 | // \ / 77 | // This is all we need 78 | let pieces = decrypted.split('$'); 79 | let alg = pieces[1]; 80 | let params = pieces[3]; 81 | 82 | let result = await sodium.sodium_memcmp( 83 | Buffer.from(this.options.alg), 84 | Buffer.from(alg) 85 | ); 86 | return result && await sodium.sodium_memcmp( 87 | Buffer.from(encoded), 88 | Buffer.from(params) 89 | ); 90 | } 91 | 92 | /** 93 | * @param {string|Buffer} password 94 | * @param {string|Buffer} pwhash 95 | * @param {string|Buffer} ad 96 | * @return {boolean} 97 | */ 98 | async verify(password, pwhash, ad = '') { 99 | if (!sodium) sodium = await SodiumPlus.auto(); 100 | let decrypted; 101 | if (ad.length > 0) { 102 | decrypted = await Symmetric.decryptWithAd(pwhash, this.symmetricKey, ad); 103 | } else { 104 | decrypted = await Symmetric.decrypt(pwhash, this.symmetricKey); 105 | } 106 | return sodium.crypto_pwhash_str_verify( 107 | password, 108 | decrypted.toString(), 109 | ); 110 | } 111 | }; 112 | -------------------------------------------------------------------------------- /test/symmetric-test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const expect = require('chai').expect; 3 | const Symmetric = require('../lib/Symmetric'); 4 | const SymmetricKey = require('../lib/key/SymmetricKey'); 5 | const Util = require('../lib/Util'); 6 | const base64url = require('rfc4648').base64url; 7 | const hex = require('rfc4648').base16; 8 | const loadJsonFile = require('load-json-file'); 9 | const expectError = require('./async-test-helper'); 10 | 11 | describe('Symmetric.auth()', function () { 12 | it('should authenticate a message', async function() { 13 | let symKey = new SymmetricKey( 14 | hex.parse("146e4cc92d60bd163c8d8eb0468734cc3c3b7ae7616cbc690c721fd5b08370cf") 15 | ); 16 | let message = "This is a test message."; 17 | 18 | let tag = await Symmetric.auth(message, symKey); 19 | let check = await Symmetric.verify(message, tag, symKey); 20 | expect(check).to.be.equal(true); 21 | await expectError( 22 | Symmetric.verify(message, tag.slice(1), symKey), 23 | 'MAC is not sufficient in length' 24 | ); 25 | }); 26 | 27 | it('should pass the standard test vectors', async function() { 28 | let json = await loadJsonFile('./test/test-vectors.json'); 29 | let keys = {}; 30 | let k; 31 | for (k in json.symmetric.keys) { 32 | keys[k] = new SymmetricKey( 33 | base64url.parse(json.symmetric.keys[k]) 34 | ); 35 | } 36 | 37 | let key; 38 | let test; 39 | let check; 40 | for (let i = 0; i < json.symmetric.auth.length; i++) { 41 | test = json.symmetric.auth[i]; 42 | key = keys[test.key]; 43 | check = await Symmetric.verify(test.message, test.mac, key); 44 | expect(check).to.be.equal(true); 45 | } 46 | }); 47 | }); 48 | 49 | describe('Symmetric.encrypt', function() { 50 | it('should reject invalid ciphertexts', async function () { 51 | expect(false).to.be.equal(Symmetric.isValidCiphertext('')); 52 | }); 53 | it('should encrypt a message', async function() { 54 | let symKey = new SymmetricKey( 55 | hex.parse("146e4cc92d60bd163c8d8eb0468734cc3c3b7ae7616cbc690c721fd5b08370cf") 56 | ); 57 | let message = "This is a test message."; 58 | 59 | let cipher = await Symmetric.encrypt(message, symKey); 60 | let decrypt = await Symmetric.decrypt(cipher, symKey); 61 | expect(decrypt.toString()).to.be.equal(message); 62 | await expectError( 63 | Symmetric.encryptWithAd(cipher, ''), 64 | 'Argument 2 must be a SymmetricKey' 65 | ); 66 | await expectError( 67 | Symmetric.decryptWithAd(cipher, ''), 68 | 'Argument 2 must be a SymmetricKey' 69 | ); 70 | await expectError( 71 | Symmetric.decryptWithAd(cipher.slice(0, 7), symKey), 72 | 'Ciphertext is too short' 73 | ); 74 | await expectError( 75 | Symmetric.decryptWithAd(cipher.slice(8), symKey), 76 | 'Invalid header' 77 | ); 78 | }); 79 | 80 | it('should pass the standard test vectors', async function() { 81 | let json = await loadJsonFile('./test/test-vectors.json'); 82 | let keys = {}; 83 | let k; 84 | for (k in json.symmetric.keys) { 85 | keys[k] = new SymmetricKey( 86 | Util.stringToBuffer(base64url.parse(json.symmetric.keys[k])) 87 | ); 88 | } 89 | 90 | let key; 91 | let test; 92 | let check; 93 | for (let i = 0; i < json.symmetric.encrypt.length; i++) { 94 | test = json.symmetric.encrypt[i]; 95 | key = keys[test.key]; 96 | try { 97 | expect(true).to.be.equal(Symmetric.isValidCiphertext(test.encrypted)); 98 | check = await Symmetric.decryptWithAd( 99 | test.encrypted, 100 | key, 101 | test.aad 102 | ); 103 | } catch (e) { 104 | console.log("Failure at index " + i); 105 | throw e; 106 | } 107 | expect(check.toString()).to.be.equal(test.decrypted); 108 | } 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /test/keyring-test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const expect = require('chai').expect; 3 | const AsymmetricSecretKey = require('../lib/key/AsymmetricSecretKey'); 4 | const AsymmetricPublicKey = require('../lib/key/AsymmetricPublicKey'); 5 | const SymmetricKey = require('../lib/key/SymmetricKey'); 6 | const Keyring = require('../lib/Keyring'); 7 | const loadJsonFile = require('load-json-file'); 8 | const { SodiumPlus, CryptographyKey } = require('sodium-plus'); 9 | let sodium; 10 | 11 | describe('Keyring', function() { 12 | it('should pass the standard test vectors', async function () { 13 | if (!sodium) sodium = await SodiumPlus.auto(); 14 | 15 | let json = await loadJsonFile('./test/test-vectors.json'); 16 | let blake2bFox = await sodium.crypto_generichash('red fox (vulpes vulpes)'); 17 | let blake2bWolf = await sodium.crypto_generichash('timber wolf (canis lupus)'); 18 | let blake2bDhole = await sodium.crypto_generichash('dhole (cuon alpinus)'); 19 | let blake2bUwU = await sodium.crypto_generichash('wrap my keys UwU'); 20 | let symKeywrap = new SymmetricKey(blake2bUwU); 21 | let symDhole = new SymmetricKey(blake2bDhole); 22 | let foxSecret = Buffer.alloc(64); 23 | let foxPublic = Buffer.alloc(32); 24 | let wolfSecret = Buffer.alloc(64); 25 | let wolfPublic = Buffer.alloc(32); 26 | let foxKeypair = await sodium.crypto_sign_seed_keypair(blake2bFox); 27 | let wolfKeypair = await sodium.crypto_sign_seed_keypair(blake2bWolf); 28 | foxSecret = new AsymmetricSecretKey(foxKeypair.slice(0, 64)); 29 | foxPublic = new AsymmetricPublicKey(foxKeypair.slice(64, 96)); 30 | wolfSecret = new AsymmetricSecretKey(wolfKeypair.slice(0, 64)); 31 | wolfPublic = new AsymmetricPublicKey(wolfKeypair.slice(64, 96)); 32 | 33 | let keyring0 = new Keyring(); 34 | let keyring1 = new Keyring(symKeywrap); 35 | let decoded; 36 | 37 | // Save 38 | expect(await keyring0.save(foxSecret)).to.be.equal(json['key-ring']['non-wrapped']['fox-secret-key']); 39 | expect(await keyring0.save(foxPublic)).to.be.equal(json['key-ring']['non-wrapped']['fox-public-key']); 40 | expect(await keyring0.save(wolfSecret)).to.be.equal(json['key-ring']['non-wrapped']['wolf-secret-key']); 41 | expect(await keyring0.save(wolfPublic)).to.be.equal(json['key-ring']['non-wrapped']['wolf-public-key']); 42 | expect(await keyring0.save(symDhole)).to.be.equal(json['key-ring']['non-wrapped']['symmetric-default']); 43 | 44 | // Load (unwrapped) 45 | decoded = await keyring0.load(json['key-ring']['non-wrapped']['fox-secret-key']); 46 | assert(decoded instanceof AsymmetricSecretKey); 47 | decoded = await keyring0.load(json['key-ring']['non-wrapped']['fox-public-key']); 48 | assert(decoded instanceof AsymmetricPublicKey); 49 | decoded = await keyring0.load(json['key-ring']['non-wrapped']['wolf-secret-key']); 50 | assert(decoded instanceof AsymmetricSecretKey); 51 | decoded = await keyring0.load(json['key-ring']['non-wrapped']['wolf-public-key']); 52 | assert(decoded instanceof AsymmetricPublicKey); 53 | decoded = await keyring0.load(json['key-ring']['non-wrapped']['symmetric-default']); 54 | assert(decoded instanceof SymmetricKey); 55 | 56 | // Load (unwrapped, but with key) 57 | decoded = await keyring1.load(json['key-ring']['non-wrapped']['fox-secret-key']); 58 | assert(decoded instanceof AsymmetricSecretKey); 59 | decoded = await keyring1.load(json['key-ring']['non-wrapped']['fox-public-key']); 60 | assert(decoded instanceof AsymmetricPublicKey); 61 | decoded = await keyring1.load(json['key-ring']['non-wrapped']['wolf-secret-key']); 62 | assert(decoded instanceof AsymmetricSecretKey); 63 | decoded = await keyring1.load(json['key-ring']['non-wrapped']['wolf-public-key']); 64 | assert(decoded instanceof AsymmetricPublicKey); 65 | decoded = await keyring1.load(json['key-ring']['non-wrapped']['symmetric-default']); 66 | assert(decoded instanceof SymmetricKey); 67 | 68 | // Load (wrapped) 69 | decoded = await keyring1.load(json['key-ring']['wrapped']['fox-secret-key']); 70 | assert(decoded instanceof AsymmetricSecretKey); 71 | decoded = await keyring1.load(json['key-ring']['wrapped']['fox-public-key']); 72 | assert(decoded instanceof AsymmetricPublicKey); 73 | decoded = await keyring1.load(json['key-ring']['wrapped']['wolf-secret-key']); 74 | assert(decoded instanceof AsymmetricSecretKey); 75 | decoded = await keyring1.load(json['key-ring']['wrapped']['wolf-public-key']); 76 | assert(decoded instanceof AsymmetricPublicKey); 77 | decoded = await keyring1.load(json['key-ring']['wrapped']['symmetric-default']); 78 | assert(decoded instanceof SymmetricKey); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /lib/Symmetric.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const base64url = require('rfc4648').base64url; 4 | const Util = require('./Util'); 5 | const CryptoError = require('./error/CryptoError'); 6 | const { SodiumPlus, CryptographyKey } = require('sodium-plus'); 7 | const SymmetricKey = require('./key/SymmetricKey'); 8 | 9 | const HEADER = "dhole100"; 10 | const ALLOWED_HEADERS = ["dhole100"]; 11 | const DOMAIN_SEPARATION = Buffer.from("DHOLEcrypto-Domain5eparatorConstant"); 12 | 13 | let sodium; 14 | 15 | /** 16 | * @name Symmetric 17 | * @package dholecrypto 18 | */ 19 | module.exports = class Symmetric 20 | { 21 | /** 22 | * @param {string|Buffer} message 23 | * @param {SymmetricKey} symKey 24 | * @returns {string} 25 | */ 26 | static async auth(message, symKey) { 27 | if (!sodium) sodium = await SodiumPlus.auto(); 28 | message = Util.stringToBuffer(message); 29 | let subkey = await sodium.crypto_generichash( 30 | symKey.getBuffer(), 31 | new CryptographyKey(DOMAIN_SEPARATION) 32 | ); 33 | let output = await sodium.crypto_auth( 34 | message, 35 | new CryptographyKey(subkey) 36 | ); 37 | return output.toString('hex'); 38 | } 39 | 40 | /** 41 | * 42 | * @param {string|Buffer} plaintext 43 | * @param {SymmetricKey} symKey 44 | * @returns {string} 45 | */ 46 | static async encrypt(plaintext, symKey) { 47 | return Symmetric.encryptWithAd(plaintext, symKey, ""); 48 | } 49 | 50 | /** 51 | * 52 | * @param {string|Buffer} ciphertext 53 | * @param {SymmetricKey} symKey 54 | * @returns {string} 55 | */ 56 | static async decrypt(ciphertext, symKey) { 57 | return Symmetric.decryptWithAd(ciphertext, symKey, ""); 58 | } 59 | 60 | /** 61 | * 62 | * @param {string|Buffer} plaintext 63 | * @param {SymmetricKey} symKey 64 | * @param {string|Buffer} aad 65 | * @returns {string} 66 | */ 67 | static async encryptWithAd(plaintext, symKey, aad = "") { 68 | if (!sodium) sodium = await SodiumPlus.auto(); 69 | if (!(symKey instanceof SymmetricKey)) { 70 | throw new TypeError('Argument 2 must be a SymmetricKey'); 71 | } 72 | plaintext = Util.stringToBuffer(plaintext); 73 | aad = Util.stringToBuffer(aad); 74 | let nonce = await sodium.randombytes_buf(24); 75 | let ad; 76 | if (aad.length >= 1) { 77 | ad = Buffer.concat([Buffer.from(HEADER), nonce, aad]); 78 | } else { 79 | ad = Buffer.concat([Buffer.from(HEADER), nonce]); 80 | } 81 | let ciphertext = await sodium.crypto_aead_xchacha20poly1305_ietf_encrypt( 82 | plaintext, 83 | nonce, 84 | symKey, 85 | ad 86 | ); 87 | 88 | return HEADER + base64url.stringify( 89 | Buffer.from( 90 | nonce.toString('binary') + ciphertext.toString('binary'), 91 | 'binary' 92 | ) 93 | ); 94 | } 95 | 96 | /** 97 | * 98 | * @param {string|Buffer} ciphertext 99 | * @param {SymmetricKey} symKey 100 | * @param {string|Buffer} aad 101 | * @returns {string} 102 | */ 103 | static async decryptWithAd(ciphertext, symKey, aad = "") { 104 | if (!sodium) sodium = await SodiumPlus.auto(); 105 | if (!(symKey instanceof SymmetricKey)) { 106 | throw new TypeError('Argument 2 must be a SymmetricKey'); 107 | } 108 | ciphertext = Util.stringToBuffer(ciphertext); 109 | aad = Util.stringToBuffer(aad); 110 | if (ciphertext.length < 8) { 111 | throw new CryptoError("Ciphertext is too short"); 112 | } 113 | let header = ciphertext.slice(0, 8).toString(); 114 | if (!ALLOWED_HEADERS.includes(header)) { 115 | throw new CryptoError("Invalid header"); 116 | } 117 | 118 | let decoded = Util.stringToBuffer( 119 | base64url.parse(ciphertext.slice(8).toString()) 120 | ); 121 | let nonce = decoded.slice(0, 24); 122 | let cipher = decoded.slice(24); 123 | 124 | let ad; 125 | if (aad.length >= 1) { 126 | ad = Buffer.concat([Buffer.from(HEADER), nonce, aad]); 127 | } else { 128 | ad = Buffer.concat([Buffer.from(HEADER), nonce]); 129 | } 130 | 131 | try { 132 | return sodium.crypto_aead_xchacha20poly1305_ietf_decrypt( 133 | cipher, 134 | nonce, 135 | symKey, 136 | ad 137 | ); 138 | } catch (e) { 139 | /* istanbul ignore next */ 140 | throw new CryptoError("Decryption failed"); 141 | } 142 | } 143 | 144 | /** 145 | * @param {string|Buffer} message 146 | * @param {string|Buffer} mac 147 | * @param {SymmetricKey} symKey 148 | * @returns {boolean} 149 | */ 150 | static async verify(message, mac, symKey) { 151 | if (!sodium) sodium = await SodiumPlus.auto(); 152 | message = Util.stringToBuffer(message); 153 | mac = Buffer.from(mac, 'hex'); 154 | if (mac.length !== sodium.CRYPTO_AUTH_BYTES) { 155 | throw new CryptoError("MAC is not sufficient in length"); 156 | } 157 | let subkey = await sodium.crypto_generichash( 158 | symKey.getBuffer(), 159 | new CryptographyKey(DOMAIN_SEPARATION) 160 | ); 161 | return sodium.crypto_auth_verify( 162 | message, 163 | new CryptographyKey(subkey), 164 | mac 165 | ); 166 | } 167 | 168 | /** 169 | * @param {string|Buffer} ciphertext 170 | * @returns {boolean} 171 | */ 172 | static isValidCiphertext(ciphertext) { 173 | ciphertext = Util.stringToBuffer(ciphertext); 174 | if (ciphertext.length < 8) { 175 | return false; 176 | } 177 | let header = ciphertext.slice(0, 8).toString(); 178 | return ALLOWED_HEADERS.includes(header); 179 | } 180 | }; 181 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DholeCrypto.js 2 | 3 | [![Support me on Patreon](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.vercel.app%2Fapi%3Fusername%3Dsoatok%26type%3Dpatrons&style=for-the-badge)](https://patreon.com/soatok) 4 | 5 | [![Travis CI](https://travis-ci.org/soatok/dholecrypto-js.svg?branch=master)](https://travis-ci.org/soatok/dholecrypto-js) 6 | [![npm version](https://img.shields.io/npm/v/dhole-crypto.svg)](https://npm.im/dhole-crypto) 7 | 8 | JavaScript port of [Dhole Cryptography](https://github.com/soatok/dhole-cryptography) (PHP). 9 | 10 | Libsodium wrapper for Soatok's JavaScript projects. Released under the very 11 | permissive ISC license. 12 | 13 | > Important: Until version `v1.0.0` is released, please don't deploy this library 14 | > in any production systems. I'll tag `v1.0.0` when I'm confident in the correctness 15 | > and security of the implementation. 16 | 17 | ## Installation 18 | 19 | ``` 20 | npm install dhole-crypto 21 | ``` 22 | 23 | ## Usage 24 | 25 | ### Asymmetric Cryptography 26 | 27 | #### Digital Signatures 28 | 29 | ```javascript 30 | const { 31 | Asymmetric, 32 | AsymmetricSecretKey 33 | } = require('dhole-crypto'); 34 | 35 | (async function () { 36 | let wolfSecret = await AsymmetricSecretKey.generate(); 37 | let wolfPublic = wolfSecret.getPublicKey(); 38 | 39 | let message = "Your $350 awoo fine has been paid UwU"; 40 | 41 | let signature = await Asymmetric.sign(message, wolfSecret); 42 | if (!await Asymmetric.verify(message, wolfPublic, signature)) { 43 | console.log("Invalid signature. Awoo not authorized."); 44 | } 45 | })(); 46 | ``` 47 | 48 | #### Authenticated Public-Key Encryption 49 | 50 | ```javascript 51 | const { 52 | Asymmetric, 53 | AsymmetricSecretKey 54 | } = require('dhole-crypto'); 55 | 56 | (async function () { 57 | let foxSecret = await AsymmetricSecretKey.generate(); 58 | let foxPublic = foxSecret.getPublicKey(); 59 | 60 | let wolfSecret = await AsymmetricSecretKey.generate(); 61 | let wolfPublic = wolfSecret.getPublicKey(); 62 | 63 | let message = "Encrypt me UwU"; 64 | let encrypted = await Asymmetric.encrypt(message, foxPublic, wolfSecret); 65 | let decrypted = await Asymmetric.decrypt(encrypted, foxSecret, wolfPublic); 66 | console.log(decrypted.toString()); // "Encrypt me UwU" 67 | })(); 68 | ``` 69 | 70 | #### Anonymous Public-Key Encryption 71 | 72 | ```javascript 73 | const { 74 | Asymmetric, 75 | AsymmetricSecretKey 76 | } = require('dhole-crypto'); 77 | 78 | (async function () { 79 | let foxSecret = await AsymmetricSecretKey.generate(); 80 | let foxPublic = foxSecret.getPublicKey(); 81 | 82 | let message = "Encrypt me UwU"; 83 | let encrypted = await Asymmetric.seal(message, foxPublic); 84 | let decrypted = await Asymmetric.unseal(encrypted, foxSecret); 85 | console.log(decrypted.toString()); // "Encrypt me UwU" 86 | })(); 87 | ``` 88 | 89 | ### Symmetric Cryptography 90 | 91 | #### Encryption 92 | 93 | ```javascript 94 | const { 95 | Symmetric, 96 | SymmetricKey 97 | } = require('dhole-crypto'); 98 | 99 | (async function () { 100 | let symmetricKey = await SymmetricKey.generate(); 101 | 102 | let message = "Encrypt me UwU"; 103 | let encrypted = await Symmetric.encrypt(message, symmetricKey); 104 | let decrypted = await Symmetric.decrypt(encrypted, symmetricKey); 105 | console.log(decrypted); // "Encrypt me UwU" 106 | })(); 107 | ``` 108 | 109 | #### Encryption with Additional Data 110 | 111 | ```javascript 112 | const { 113 | Symmetric, 114 | SymmetricKey 115 | } = require('dhole-crypto'); 116 | 117 | (async function () { 118 | let symmetricKey = await SymmetricKey.generate(); 119 | 120 | let message = "Encrypt me UwU"; 121 | let publicData = "OwO? UwU"; 122 | let encrypted = await Symmetric.encryptWithAd(message, symmetricKey, publicData); 123 | let decrypted = await Symmetric.decryptWithAd(encrypted, symmetricKey, publicData); 124 | console.log(decrypted); // "Encrypt me UwU" 125 | })(); 126 | ``` 127 | 128 | #### Unencrypted Message Authentication 129 | 130 | ```javascript 131 | const { 132 | Symmetric, 133 | SymmetricKey 134 | } = require('dhole-crypto'); 135 | 136 | (async function () { 137 | let symmetricKey = await SymmetricKey.generate(); 138 | 139 | let message = "AWOOOOOOOOOOOO"; 140 | let mac = await Symmetric.auth(message, symmetricKey); 141 | if (!await Symmetric.verify(message, mac, symmetricKey)) { 142 | console.log("Unauthorized Awoo. $350 fine incoming"); 143 | } 144 | })(); 145 | ``` 146 | 147 | ## Password Storage 148 | 149 | ```javascript 150 | const { 151 | Password, 152 | SymmetricKey 153 | } = require('dhole-crypto'); 154 | 155 | (async function () { 156 | let symmetricKey = await SymmetricKey.generate(); 157 | let pwHandler = new Password(symmetricKey); 158 | 159 | let password = 'cowwect howse battewy staple UwU'; 160 | let pwhash = await pwHandler.hash(password); 161 | if (!await pwHandler.verify(password, pwhash)) { 162 | console.log("access denied"); 163 | } 164 | })(); 165 | ``` 166 | 167 | ## Keyring 168 | 169 | You can serialize any key by using the `Keyring` class. 170 | 171 | ```javascript 172 | const { 173 | AsymmetricSecretKey, 174 | Keyring, 175 | SymmetricKey 176 | } = require('dhole-crypto'); 177 | 178 | (async function () { 179 | let foxSecret = await AsymmetricSecretKey.generate(); 180 | let foxPublic = foxSecret.getPublicKey(); 181 | let symmetric = await SymmetricKey.generate(); 182 | // Load a serializer 183 | let ring = new Keyring(); 184 | 185 | // Serialize to string 186 | let sk = await ring.save(foxSecret); 187 | let pk = await ring.save(foxPublic); 188 | let key = await ring.save(symmetric); 189 | 190 | // Load from string 191 | let loadSk = await ring.load(sk); 192 | let loadPk = await ring.load(pk); 193 | let loadSym = await ring.load(key); 194 | })(); 195 | ``` 196 | 197 | The `Keyring` class also supports keywrap. Simply pass a separate `SymmetricKey` 198 | instance to the constructor to get wrapped keys. 199 | 200 | ```javascript 201 | const { 202 | AsymmetricSecretKey, 203 | Keyring, 204 | SymmetricKey 205 | } = require('dhole-crypto'); 206 | 207 | (async function () { 208 | // Keywrap key... 209 | let wrap = await SymemtricKey.generate(); 210 | 211 | let foxSecret = await AsymmetricSecretKey.generate(); 212 | let foxPublic = foxSecret.getPublicKey(); 213 | let symmetric = await SymmetricKey.generate(); 214 | 215 | // Load a serializer 216 | let ring = new Keyring(wrap); 217 | 218 | // Serialize to string 219 | let sk = await ring.save(foxSecret); 220 | let pk = await ring.save(foxPublic); 221 | let key = await ring.save(symmetric); 222 | 223 | // Load from string 224 | let loadSk = await ring.load(sk); 225 | let loadPk = await ring.load(pk); 226 | let loadSym = await ring.load(key); 227 | })(); 228 | ``` 229 | 230 | # Support 231 | 232 | If you run into any trouble using this library, or something breaks, 233 | feel free to file a Github issue. 234 | 235 | If you need help with integration, [Soatok is available for freelance work](https://soatok.com/freelance). 236 | -------------------------------------------------------------------------------- /lib/Asymmetric.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const base64url = require('rfc4648').base64url; 4 | const { SodiumPlus, Ed25519SecretKey, X25519PublicKey} = require('sodium-plus'); 5 | const Util = require('./Util'); 6 | const CryptoError = require('./error/CryptoError'); 7 | const AsymmetricPublicKey = require('./key/AsymmetricPublicKey'); 8 | const AsymmetricSecretKey = require('./key/AsymmetricSecretKey'); 9 | const SymmetricKey = require('./key/SymmetricKey'); 10 | const Symmetric = require('./Symmetric'); 11 | 12 | let sodium; 13 | /** 14 | * @name Symmetric 15 | * @package dholecrypto 16 | */ 17 | module.exports = class Asymmetric { 18 | /** 19 | * 20 | * @param {AsymmetricSecretKey} sk 21 | * @param {AsymmetricPublicKey} pk 22 | * @param {boolean} isClient 23 | * @return {SymmetricKey} 24 | */ 25 | static async keyExchange(sk, pk, isClient) { 26 | if (!sodium) sodium = await SodiumPlus.auto(); 27 | if (!(sk instanceof AsymmetricSecretKey)) { 28 | throw new TypeError("Argument 0 must be an instance of AsymmetricSecretKey."); 29 | } 30 | if (!(pk instanceof AsymmetricPublicKey)) { 31 | throw new TypeError("Argument 1 must be an instance of AsymmetricPublicKey."); 32 | } 33 | // X25519 34 | let output; 35 | let shared = await sodium.crypto_scalarmult( 36 | await sk.getBirationalSecret(), 37 | await pk.getBirationalPublic() 38 | ); 39 | if (isClient) { 40 | // BLAKE2b 41 | output = await sodium.crypto_generichash( 42 | Buffer.concat([ 43 | shared.getBuffer(), 44 | (await sk.getPublicKey().getBirationalPublic()).getBuffer(), 45 | (await pk.getBirationalPublic()).getBuffer() 46 | ]) 47 | ); 48 | } else { 49 | // BLAKE2b 50 | output = await sodium.crypto_generichash( 51 | Buffer.concat([ 52 | shared.getBuffer(), 53 | (await pk.getBirationalPublic()).getBuffer(), 54 | (await sk.getPublicKey().getBirationalPublic()).getBuffer() 55 | ]) 56 | ); 57 | } 58 | return new SymmetricKey(output); 59 | } 60 | 61 | /** 62 | * @param {string|Buffer} msg 63 | * @param {AsymmetricPublicKey} pk 64 | * @param {AsymmetricSecretKey} sk 65 | * @return {string} 66 | */ 67 | static async encrypt(msg, pk, sk) { 68 | msg = Util.stringToBuffer(msg); 69 | if (!(pk instanceof AsymmetricPublicKey)) { 70 | throw new TypeError("Argument 2 must be an instance of AsymmetricPublicKey."); 71 | } 72 | if (!(sk instanceof AsymmetricSecretKey)) { 73 | throw new TypeError("Argument 3 must be an instance of AsymmetricSecretKey."); 74 | } 75 | return Asymmetric.seal( 76 | (await Asymmetric.sign(msg, sk)) + msg, 77 | pk 78 | ); 79 | } 80 | 81 | /** 82 | * @param {string|Buffer} msg 83 | * @param {AsymmetricSecretKey} sk 84 | * @param {AsymmetricPublicKey} pk 85 | * @return {string} 86 | */ 87 | static async decrypt (msg, sk, pk) { 88 | msg = Util.stringToBuffer(msg); 89 | if (!(sk instanceof AsymmetricSecretKey)) { 90 | throw new TypeError("Argument 2 must be an instance of AsymmetricSecretKey."); 91 | } 92 | if (!(pk instanceof AsymmetricPublicKey)) { 93 | throw new TypeError("Argument 3 must be an instance of AsymmetricPublicKey."); 94 | } 95 | let unsealed = await Asymmetric.unseal(msg, sk); 96 | let signature = unsealed.slice(0, 128); 97 | let plaintext = unsealed.slice(128); 98 | if (!Asymmetric.verify(plaintext, pk, signature)) { 99 | throw new CryptoError("Invalid signature"); 100 | } 101 | return plaintext.toString('binary'); 102 | } 103 | 104 | /** 105 | * @param {string|Buffer} msg 106 | * @param {AsymmetricPublicKey} pk 107 | * @return {string} 108 | */ 109 | static async seal(msg, pk) { 110 | msg = Util.stringToBuffer(msg); 111 | if (!(pk instanceof AsymmetricPublicKey)) { 112 | throw new TypeError("Argument 2 must be an instance of AsymmetricPublicKey."); 113 | } 114 | let sk = await AsymmetricSecretKey.generate(); 115 | let sym = await Asymmetric.keyExchange(sk, pk, true); 116 | let pub = await sk.getPublicKey().getBirationalPublic(); 117 | return (await Symmetric.encryptWithAd(msg, sym, pub.getBuffer())) 118 | + '$' + 119 | base64url.stringify(pub.getBuffer()); 120 | } 121 | 122 | /** 123 | * @param {string|Buffer} msg 124 | * @param {AsymmetricSecretKey} sk 125 | * @return {string} 126 | */ 127 | static async unseal(msg, sk) { 128 | msg = Util.stringToBuffer(msg); 129 | if (!(sk instanceof AsymmetricSecretKey)) { 130 | throw new TypeError("Argument 2 must be an instance of AsymmetricSecretKey."); 131 | } 132 | let pos = msg.toString().indexOf('$'); 133 | if (pos < 0) { 134 | throw new CryptoError("Invalid ciphertext: Not sealed"); 135 | } 136 | let cipher = msg.slice(0, pos); 137 | let buf = Util.stringToBuffer( 138 | base64url.parse(msg.slice(pos + 1).toString()) 139 | ); 140 | if (buf.length !== 32) { 141 | throw new CryptoError(`Invalid public key size: ${buf.length}`); 142 | } 143 | let pk = new X25519PublicKey(buf); 144 | 145 | let shared = await sodium.crypto_scalarmult( 146 | await sk.getBirationalSecret(), 147 | pk 148 | ); 149 | let sym = await sodium.crypto_generichash( 150 | Buffer.concat([ 151 | shared.getBuffer(), 152 | pk.getBuffer(), 153 | (await sk.getPublicKey().getBirationalPublic()).getBuffer() 154 | ]) 155 | ); 156 | 157 | return Symmetric.decryptWithAd( 158 | cipher, 159 | new SymmetricKey(sym), 160 | pk.getBuffer() 161 | ); 162 | } 163 | 164 | /** 165 | * @param {string|Buffer} msg 166 | * @param {AsymmetricSecretKey} sk 167 | * @return {string} 168 | */ 169 | static async sign(msg, sk) { 170 | if (!sodium) sodium = await SodiumPlus.auto(); 171 | msg = Util.stringToBuffer(msg); 172 | if (!(sk instanceof AsymmetricSecretKey)) { 173 | throw new TypeError("Argument 2 must be an instance of AsymmetricSecretKey."); 174 | } 175 | if (!(sk instanceof Ed25519SecretKey)) { 176 | throw new TypeError("Argument 2 must be an instance of Ed25519SecretKey."); 177 | } 178 | let entropy = await sodium.randombytes_buf(32); 179 | let signature = await sodium.crypto_sign_detached( 180 | Buffer.concat([entropy, msg]), 181 | sk 182 | ); 183 | return base64url.stringify( 184 | Buffer.concat([signature, entropy]) 185 | ); 186 | } 187 | 188 | /** 189 | * @param {string|Buffer} msg 190 | * @param {AsymmetricPublicKey} pk 191 | * @param {string|Buffer} signature 192 | * @return {boolean} 193 | */ 194 | static async verify(msg, pk, signature) { 195 | msg = Util.stringToBuffer(msg); 196 | if (!(pk instanceof AsymmetricPublicKey)) { 197 | throw new TypeError("Argument 2 must be an instance of AsymmetricPublicKey."); 198 | } 199 | let decoded = Util.stringToBuffer(base64url.parse(signature.toString())); 200 | let sig = decoded.slice(0, 64); 201 | let entropy = decoded.slice(64, 96); 202 | return sodium.crypto_sign_verify_detached( 203 | Buffer.concat([entropy, msg]), 204 | pk, 205 | sig 206 | ); 207 | } 208 | }; 209 | -------------------------------------------------------------------------------- /lib/Keyring.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const base64url = require('rfc4648').base64url; 4 | const AsymmetricPublicKey = require('./key/AsymmetricPublicKey'); 5 | const AsymmetricSecretKey = require('./key/AsymmetricSecretKey'); 6 | const CryptoError = require('./error/CryptoError'); 7 | const SymmetricKey = require('./key/SymmetricKey'); 8 | const Symmetric = require('./Symmetric'); 9 | const Util = require('./Util'); 10 | const { SodiumPlus } = require('sodium-plus'); 11 | let sodium; 12 | 13 | 14 | const KTYPE_ASYMMETRIC_SECRET = 'ed25519sk'; 15 | const KTYPE_ASYMMETRIC_PUBLIC = 'ed25519pk'; 16 | const KTYPE_SYMMETRIC = 'symmetric'; 17 | 18 | module.exports = class Keyring { 19 | constructor(symKey = null) { 20 | if (symKey instanceof SymmetricKey) { 21 | this.keywrapKey = symKey; 22 | } else { 23 | this.keywrapKey = null; 24 | } 25 | } 26 | 27 | /** 28 | * @param {string} str 29 | * @returns {Buffer[]} 30 | */ 31 | getComponents(str) { 32 | if (Buffer.isBuffer(str)) { 33 | str = str.toString('binary'); 34 | } 35 | let header = Buffer.from(str.slice(0, 9), 'binary'); 36 | let decoded = Util.stringToBuffer(base64url.parse(str.slice(9))); 37 | let checksum = decoded.slice(0, 16); 38 | let body = decoded.slice(16); 39 | return [header, body, checksum]; 40 | } 41 | 42 | /** 43 | * Load a key from a string 44 | * 45 | * @param {string} str 46 | * @return {SymmetricKey|AsymmetricSecretKey|AsymmetricPublicKey} 47 | */ 48 | async load(str) { 49 | if (str.length < 9) { 50 | throw new CryptoError("String is too short to be a serialized key"); 51 | } 52 | 53 | // Handle keywrap (but still decode unwrapped keys) 54 | if (Symmetric.isValidCiphertext(str)) { 55 | if (this.keywrapKey instanceof SymmetricKey) { 56 | str = await Symmetric.decrypt(str, this.keywrapKey); 57 | } else { 58 | throw new CryptoError("This key has been encrypted and you have not provided the keywrap key."); 59 | } 60 | } 61 | 62 | let header = str.slice(0, 9); 63 | if (Util.hashEquals(header, KTYPE_SYMMETRIC)) { 64 | return this.loadSymmetricKey(str); 65 | } 66 | if (Util.hashEquals(header, KTYPE_ASYMMETRIC_SECRET)) { 67 | return this.loadAsymmetricSecretKey(str); 68 | } 69 | if (Util.hashEquals(header, KTYPE_ASYMMETRIC_PUBLIC)) { 70 | return this.loadAsymmetricPublicKey(str); 71 | } 72 | throw new CryptoError("Invalid key header"); 73 | } 74 | 75 | /** 76 | * Load a key from a string 77 | * 78 | * @param {string} str 79 | * @return {AsymmetricSecretKey} 80 | */ 81 | async loadAsymmetricSecretKey(str) { 82 | let header, body, checksum, calc; 83 | [header, body, checksum] = this.getComponents(str); 84 | calc = await sodium.crypto_generichash( 85 | Buffer.concat([header, body]), 86 | null, 87 | 16 88 | ); 89 | if (!Util.hashEquals(calc, checksum)) { 90 | throw new CryptoError("Checksum failed. Corrupt key?"); 91 | } 92 | if (body.length < 96) { 93 | throw new CryptoError("Invalid key length."); 94 | } 95 | let ret = new AsymmetricSecretKey(body.slice(0, 64)); 96 | ret.injectBirationalEquivalent(body.slice(64,96)); 97 | return ret; 98 | } 99 | 100 | 101 | /** 102 | * Load a key from a string 103 | * 104 | * @param {string} str 105 | * @return {AsymmetricPublicKey} 106 | */ 107 | async loadAsymmetricPublicKey(str) { 108 | if (!sodium) sodium = await SodiumPlus.auto(); 109 | let header, body, checksum, calc; 110 | [header, body, checksum] = this.getComponents(str); 111 | calc = await sodium.crypto_generichash( 112 | Buffer.concat([header, body]), 113 | null, 114 | 16 115 | ); 116 | if (!Util.hashEquals(calc, checksum)) { 117 | throw new CryptoError("Checksum failed. Corrupt key?"); 118 | } 119 | if (body.length < 64) { 120 | throw new CryptoError("Invalid key length."); 121 | } 122 | let ret = new AsymmetricPublicKey(body.slice(0, 32)); 123 | ret.injectBirationalEquivalent(body.slice(32, 64)); 124 | return ret; 125 | } 126 | 127 | /** 128 | * Load a key from a string 129 | * 130 | * @param {string} str 131 | * @return {SymmetricKey} 132 | */ 133 | async loadSymmetricKey(str) { 134 | if (!sodium) sodium = await SodiumPlus.auto(); 135 | let header, body, checksum, calc; 136 | [header, body, checksum] = this.getComponents(str); 137 | calc = await sodium.crypto_generichash( 138 | Buffer.concat([header, body]), 139 | null, 140 | 16 141 | ); 142 | if (!Util.hashEquals(calc, checksum)) { 143 | throw new CryptoError("Checksum failed. Corrupt key?"); 144 | } 145 | if (body.length < 32) { 146 | throw new CryptoError("Invalid key length."); 147 | } 148 | return new SymmetricKey(body); 149 | } 150 | 151 | /** 152 | * Encrypts a string with the keywrap key. If it's not defined, 153 | * this function falls back to plaintext. 154 | * 155 | * @param {string} str 156 | * @returns {string} 157 | */ 158 | keywrap(str) { 159 | if (this.keywrapKey instanceof SymmetricKey) { 160 | return Symmetric.encrypt(str, this.keywrapKey); 161 | } 162 | return str; 163 | } 164 | 165 | /** 166 | * Serialize a key for storage 167 | * 168 | * @param {SymmetricKey|AsymmetricSecretKey|AsymmetricPublicKey} key 169 | * @return string 170 | */ 171 | async save(key) { 172 | if (key instanceof AsymmetricSecretKey) { 173 | return this.keywrap( 174 | this.saveAsymmetricSecretKey(key) 175 | ); 176 | } 177 | if (key instanceof AsymmetricPublicKey) { 178 | return this.keywrap( 179 | this.saveAsymmetricPublicKey(key) 180 | ); 181 | } 182 | if (key instanceof SymmetricKey) { 183 | return this.keywrap( 184 | this.saveSymmetricKey(key) 185 | ); 186 | } 187 | throw new CryptoError("Invalid key type"); 188 | } 189 | 190 | /** 191 | * @param {AsymmetricSecretKey} key 192 | * @return {string} 193 | */ 194 | async saveAsymmetricPublicKey(key) { 195 | if (!sodium) sodium = await SodiumPlus.auto(); 196 | if (!(key instanceof AsymmetricPublicKey)) { 197 | throw new TypeError(); 198 | } 199 | let header = Buffer.from(KTYPE_ASYMMETRIC_PUBLIC, 'binary'); 200 | let birational = await key.getBirationalPublic(); 201 | let checksum = await sodium.crypto_generichash( 202 | Buffer.concat([header, key.getBuffer(), birational.getBuffer()]), 203 | null, 204 | 16 205 | ); 206 | return header + base64url.stringify( 207 | Buffer.concat([checksum, key.getBuffer(), birational.getBuffer()]) 208 | ); 209 | } 210 | 211 | /** 212 | * @param {AsymmetricSecretKey} key 213 | * @return {string} 214 | */ 215 | async saveAsymmetricSecretKey(key) { 216 | if (!sodium) sodium = await SodiumPlus.auto(); 217 | if (!(key instanceof AsymmetricSecretKey)) { 218 | throw new TypeError(); 219 | } 220 | let header = Buffer.from(KTYPE_ASYMMETRIC_SECRET, 'binary'); 221 | let birational = await key.getBirationalSecret(); 222 | let checksum = await sodium.crypto_generichash( 223 | Buffer.concat([header, key.getBuffer(), birational.getBuffer()]), 224 | null, 225 | 16 226 | ); 227 | return header + base64url.stringify( 228 | Buffer.concat([checksum, key.getBuffer(), birational.getBuffer()]) 229 | ); 230 | } 231 | 232 | /** 233 | * @param {SymmetricKey} key 234 | * @return {string} 235 | */ 236 | async saveSymmetricKey(key) { 237 | if (!sodium) sodium = await SodiumPlus.auto(); 238 | if (!(key instanceof SymmetricKey)) { 239 | throw new TypeError(); 240 | } 241 | let header = Buffer.from(KTYPE_SYMMETRIC, 'binary'); 242 | let checksum = await sodium.crypto_generichash( 243 | Buffer.concat([header, key.getBuffer()]), 244 | null, 245 | 16 246 | ); 247 | return header + base64url.stringify( 248 | Buffer.concat([checksum, key.getBuffer()]) 249 | ); 250 | } 251 | }; 252 | -------------------------------------------------------------------------------- /test/asymmetric-test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const expect = require('chai').expect; 3 | const Asymmetric = require('../lib/Asymmetric'); 4 | const AsymmetricSecretKey = require('../lib/key/AsymmetricSecretKey'); 5 | const AsymmetricPublicKey = require('../lib/key/AsymmetricPublicKey'); 6 | const SymmetricKey = require('../lib/key/SymmetricKey'); 7 | 8 | const Util = require('../lib/Util'); 9 | const base64url = require('rfc4648').base64url; 10 | const hex = require('rfc4648').base16; 11 | const loadJsonFile = require('load-json-file'); 12 | 13 | describe('Asymmetric.encrypt()', function() { 14 | it('should allow messages to encrypt', async function() { 15 | let aliceSk = await AsymmetricSecretKey.generate(); 16 | let alicePk = await aliceSk.getPublicKey(); 17 | let bobSk = await AsymmetricSecretKey.generate(); 18 | let bobPk = await bobSk.getPublicKey(); 19 | 20 | let message = "This is a super secret message UwU"; 21 | let encrypted = await Asymmetric.encrypt(message, alicePk, bobSk); 22 | let decrypted = await Asymmetric.decrypt(encrypted, aliceSk, bobPk); 23 | expect(message.toString()).to.be.equal(decrypted.toString()); 24 | }); 25 | 26 | it('should pass the standard test vectors', async function () { 27 | let json = await loadJsonFile('./test/test-vectors.json'); 28 | let participants = {}; 29 | let test; 30 | 31 | // Load all of our participants... 32 | let k; 33 | let t; 34 | for (k in json.asymmetric.participants) { 35 | participants[k] = {}; 36 | participants[k].sk = new AsymmetricSecretKey( 37 | base64url.parse(json.asymmetric.participants[k]['secret-key']) 38 | ); 39 | participants[k].pk = new AsymmetricPublicKey( 40 | base64url.parse(json.asymmetric.participants[k]['public-key']) 41 | ); 42 | } 43 | 44 | let result; 45 | for (t = 0; t < json.asymmetric.encrypt.length; t++) { 46 | test = json.asymmetric.encrypt[t]; 47 | result = await Asymmetric.decrypt( 48 | test.encrypted, 49 | participants[test.recipient].sk, 50 | participants[test.sender].pk 51 | ); 52 | expect(test.decrypted).to.be.equal(result); 53 | } 54 | }); 55 | }); 56 | 57 | describe('Asymmetric.keyExchange()', function() { 58 | it('should generate congruent shared secrets', async function() { 59 | let alice = await AsymmetricSecretKey.generate(); 60 | let bob = await AsymmetricSecretKey.generate(); 61 | 62 | let testA = (await Asymmetric.keyExchange(alice, bob.getPublicKey(), true)) 63 | .getBuffer().toString('hex'); 64 | let testB = (await Asymmetric.keyExchange(bob, alice.getPublicKey(), false)) 65 | .getBuffer().toString('hex'); 66 | let testC = (await Asymmetric.keyExchange(alice, bob.getPublicKey(), false)) 67 | .getBuffer().toString('hex'); 68 | let testD = (await Asymmetric.keyExchange(bob, alice.getPublicKey(), true)) 69 | .getBuffer().toString('hex'); 70 | 71 | // Standard sanity checks: 72 | expect(testA).to.be.equal(testB); 73 | expect(testC).to.be.equal(testD); 74 | expect(testA).to.not.be.equal(testC); 75 | expect(testB).to.not.be.equal(testD); 76 | 77 | // Extra test: Don't accept all-zero shared secrets 78 | expect(testA).to.not.be.equal( 79 | '0000000000000000000000000000000000000000000000000000000000000000' 80 | ); 81 | expect(testC).to.not.be.equal( 82 | '0000000000000000000000000000000000000000000000000000000000000000' 83 | ); 84 | }); 85 | 86 | it('should pass the standard test vectors', async function() { 87 | let json = await loadJsonFile('./test/test-vectors.json'); 88 | let participants = {}; 89 | let shared = {}; 90 | let test; 91 | 92 | // Load all of our participants... 93 | let k; 94 | for (k in json.asymmetric.participants) { 95 | participants[k] = {}; 96 | participants[k].sk = new AsymmetricSecretKey( 97 | base64url.parse(json.asymmetric.participants[k]['secret-key']) 98 | ); 99 | participants[k].pk = new AsymmetricPublicKey( 100 | base64url.parse(json.asymmetric.participants[k]['public-key']) 101 | ); 102 | expect( 103 | participants[k].sk.getPublicKey().getBuffer().toString('hex') 104 | ).to.be.equal( 105 | participants[k].pk.getBuffer().toString('hex') 106 | ); 107 | } 108 | // Let's also load up the symmetric keys to double-check our kx logic... 109 | for (k in json.symmetric.keys) { 110 | shared[k] = new SymmetricKey( 111 | base64url.parse(json.symmetric.keys[k]) 112 | ); 113 | } 114 | 115 | // Fox to Wolf 116 | test = (await Asymmetric.keyExchange( 117 | participants['fox'].sk, 118 | participants['wolf'].pk, 119 | true 120 | )).getBuffer().toString('hex'); 121 | expect(test).to.be.equal( 122 | shared['fox-to-wolf'].getBuffer().toString('hex') 123 | ); 124 | 125 | // Wolf to Fox 126 | test = (await Asymmetric.keyExchange( 127 | participants['wolf'].sk, 128 | participants['fox'].pk, 129 | true 130 | )).getBuffer().toString('hex'); 131 | expect(test).to.be.equal( 132 | shared['wolf-to-fox'].getBuffer().toString('hex') 133 | ); 134 | 135 | // Fox from Wolf 136 | test = (await Asymmetric.keyExchange( 137 | participants['fox'].sk, 138 | participants['wolf'].pk, 139 | false 140 | )).getBuffer().toString('hex'); 141 | expect(test).to.be.equal( 142 | shared['fox-from-wolf'].getBuffer().toString('hex') 143 | ); 144 | 145 | // Wolf from Fox 146 | test = (await Asymmetric.keyExchange( 147 | participants['wolf'].sk, 148 | participants['fox'].pk, 149 | false 150 | )).getBuffer().toString('hex'); 151 | expect(test).to.be.equal( 152 | shared['wolf-from-fox'].getBuffer().toString('hex') 153 | ); 154 | }); 155 | }); 156 | 157 | describe('Asymmetric.seal()', function () { 158 | it('should allow messages to seal/unseal', async function () { 159 | let aliceSk = await AsymmetricSecretKey.generate(); 160 | let alicePk = aliceSk.getPublicKey(); 161 | let message = "This is a super secret message UwU"; 162 | let sealed = await Asymmetric.seal(message, alicePk); 163 | let unseal = await Asymmetric.unseal(sealed, aliceSk); 164 | expect(message).to.be.equal(unseal.toString()); 165 | }); 166 | 167 | it('should pass the standard test vectors', async function() { 168 | let json = await loadJsonFile('./test/test-vectors.json'); 169 | let participants = {}; 170 | let test; 171 | 172 | // Load all of our participants... 173 | let k; 174 | let t; 175 | for (k in json.asymmetric.participants) { 176 | participants[k] = {}; 177 | participants[k].sk = new AsymmetricSecretKey( 178 | base64url.parse(json.asymmetric.participants[k]['secret-key']) 179 | ); 180 | participants[k].pk = new AsymmetricPublicKey( 181 | base64url.parse(json.asymmetric.participants[k]['public-key']) 182 | ); 183 | } 184 | 185 | let result; 186 | for (t = 0; t < json.asymmetric.seal.length; t++) { 187 | test = json.asymmetric.seal[t]; 188 | result = await Asymmetric.unseal( 189 | test.sealed, 190 | participants[test.recipient].sk 191 | ); 192 | expect(test.unsealed).to.be.equal(result.toString()); 193 | } 194 | }); 195 | }); 196 | 197 | describe('Asymmetric.sign()', async function () { 198 | it('should allow messages to sign/verify', async function () { 199 | let aliceSk = await AsymmetricSecretKey.generate(); 200 | let alicePk = aliceSk.getPublicKey(); 201 | let message = "This is a super secret message UwU"; 202 | let sig = await Asymmetric.sign(message, aliceSk); 203 | assert(await Asymmetric.verify(message, alicePk, sig), 'Signatures not valid'); 204 | }); 205 | 206 | it('should pass the standard test vectors', async function() { 207 | let json = await loadJsonFile('./test/test-vectors.json'); 208 | let participants = {}; 209 | let test; 210 | 211 | // Load all of our participants... 212 | let k; 213 | let t; 214 | for (k in json.asymmetric.participants) { 215 | participants[k] = {}; 216 | participants[k].sk = new AsymmetricSecretKey( 217 | base64url.parse(json.asymmetric.participants[k]['secret-key']) 218 | ); 219 | participants[k].pk = new AsymmetricPublicKey( 220 | base64url.parse(json.asymmetric.participants[k]['public-key']) 221 | ); 222 | } 223 | 224 | let signed; 225 | let result; 226 | for (t = 0; t < json.asymmetric.sign.length; t++) { 227 | test = json.asymmetric.sign[t]; 228 | signed = await Asymmetric.sign( 229 | test.message, 230 | participants[test.signer].sk 231 | ); 232 | result = await Asymmetric.verify( 233 | test.message, 234 | participants[test.signer].pk, 235 | signed 236 | ); 237 | expect(result).to.be.equal(true); 238 | 239 | result = await Asymmetric.verify( 240 | test.message, 241 | participants[test.signer].pk, 242 | test.signature 243 | ); 244 | expect(result).to.be.equal(true); 245 | } 246 | }); 247 | }); 248 | -------------------------------------------------------------------------------- /test/test-vectors.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "dhole100", 3 | "asymmetric": { 4 | "participants": { 5 | "fox": { 6 | "secret-key": "EI8iOdrfNsWt9CWG94xe_oAxa5MFQIA6K1svBwDrO4Yg6oB5NhAmpgYr9_HtTuLNIN0UJunrRAllmnEkuy1p9Q==", 7 | "public-key": "IOqAeTYQJqYGK_fx7U7izSDdFCbp60QJZZpxJLstafU=" 8 | }, 9 | "wolf": { 10 | "secret-key": "mGQSjU24v0kKw4QRFM4bfF7aS_uCbglttYp_gdf1BxZXQkhdw4HQYsBBrNI3Hob_XXdJJPp8x-IZPtnc-Uaa5w==", 11 | "public-key": "V0JIXcOB0GLAQazSNx6G_113SST6fMfiGT7Z3PlGmuc=" 12 | } 13 | }, 14 | "encrypt": [ 15 | { 16 | "sender": "wolf", 17 | "recipient": "fox", 18 | "encrypted": "dhole100PI4RnXSae37ig0X1-0XWR7U00C7ZPd3bPDXsAls-RGccQE3PDzED9C0QGp6tQcn08JjgQP9tHlOeYkO2TGrv7KevkNH80WutDc_o8bKvQKNJ5heDUg3p9jEpNKUSR8whs8taDfruylEi_nLfughkdu8kY6yzFU4hIoA98ztgyPa-XsNrZUQ6EM0q9LAzfFIkvmINHN0ed7P9t5xtlssgwAgJP1-KVfqb$y2meDvKGPgCOld-4q0FMj7Tauzl5qThBb-9XGtbaRR0=", 19 | "decrypted": "" 20 | }, 21 | { 22 | "sender": "fox", 23 | "recipient": "wolf", 24 | "encrypted": "dhole100gfDMMl755FVF6lPbLtRPdmjBP7SGgk7ZMDgu2bo_huuG9eKkxHOslG1IpkXISAs-3ASlgYYx3T5_J6Y6Et-9Q6iSnY2BgBViOdvk_BnEAwviZXMgMRkppo3W3RIYL_QLzIM9qiYCxLdRAFjAVq6O_cNPp8HOyAHxMSGju-Lt2XPIpDZZ5PVPywOUHybYQa89Q0KapmP3sPkz_k0bZ3jbsuDHqUaqexYG$xr-j5xWeIny5HFR20BHHXi4pAmaWB1sWdJojoBNELlY=", 25 | "decrypted": "" 26 | }, 27 | { 28 | "sender": "fox", 29 | "recipient": "fox", 30 | "encrypted": "dhole100GP95iv5tbHVgt3xEYfFUzrnb_Uli5dPMB4k_yhZEIWjbOjXzeSw9xmZUGWZ2cNNoLDzKTgG4nRzLYFfiGnlPrI23E2tCLjBjT0Kj8fMyTD_J1mSEGvGxP6Wc463EisynkCFOPrO30QTeyni_8Syxm1JuZRWm3cGdnYxsWoaEcYZ3UBm6WC7a380cXEQnEHJHl2QRgYydgKLdgObrfLYMtF_hhA8NtGc0$K-I7VYh01my6O58vNEczYXMaowffVKecsTz9sjyzJzA=", 31 | "decrypted": "" 32 | }, 33 | { 34 | "sender": "wolf", 35 | "recipient": "wolf", 36 | "encrypted": "dhole100igxmLQAyibIJxJmMkqCZqw_F6vIroNggKUsMfM5B3gmyhCQ1Tegy2hdJaQ4qIXdZnaraB_DdU5YJMPNZ5vabAzXHCw2e51ZtiHqQIjfIFZtWmRCyvuf3v7iZcllnbZQhu2fBG6WfFR3IqyGinlWaAKPj7IM_TeWQI7nrDJhpQQKIt_5GvzSeLjkVLMTrf5g9XK6V8JuhSwp8o534_LCJBsUMZmnR4EJA$ueeNEnXyfgj5YehN89mNEAXb565RfQD6prlRK3WiRBo=", 37 | "decrypted": "" 38 | }, 39 | { 40 | "sender": "wolf", 41 | "recipient": "fox", 42 | "encrypted": "dhole100t0AbVUhogb5FbuUwNFO5oQIyc1nd03HPuNTqD-3ApLMOFdYXyPivNn1yPY7H-1OA3Qjyv6PceTbjr3OlvPQMtOdynKeNtwZAF-PqgNexDR-89Pbc53q1h2Ybx7lKMw6Elwb6oWuFBsZP0n5VN1GoZAXQG6Crci4tsCZyhAAOz_jVKtVpI6Wur7N_uCZsw5SQdVyMzRluVj1bVkPqPoSIa6Arv8RWGv_kAGRuSZ3I-JkJ0e5CdF7pmdloGKkUnw==$rqsCzhllatq2UvnkREUfC3hOMI8NDxqvQdxhSFUkGDA=", 43 | "decrypted": "this is a test message" 44 | }, 45 | { 46 | "sender": "fox", 47 | "recipient": "wolf", 48 | "encrypted": "dhole100bIhVZpWI9MXp2HcukTQADEWNYcoIV6NtQF8pwT_JFmbcqJjAnRktV86jFKDs7HcxjFxYcAF4dsIC8YMT-S4O3_sD9HIRQ30LEmOjPfft4_DHT9DKDU4AEuD19CI2AJbyY0NmTLQYq1L23LS3kkgSVojvpaf-TmikjCIitxNDvVYThY1tnUzO3rHz_xBB8OoPx6n8DZrInZALsnjKbb6Xw3vAYlRS2krt5kCdOejsAknYq7KwfgxqD6SVs1wgQA==$Kztfq3jj_lPN4Jfa224n913Yr9W3bgBjXgTYcpSzeWo=", 49 | "decrypted": "this is a test message" 50 | }, 51 | { 52 | "sender": "fox", 53 | "recipient": "fox", 54 | "encrypted": "dhole1001HiFZO0UOv8h5xKuj5iuI1WDdLPfdofjZJFVSgnVztTUiKr8pOKpz3uvUB6PH6qqxwHqcGyPvevFsxZpY12UNYr79xhDSsYGg-A3JPAO4sy7QjFw8lzRM8Iv94jo3TP6KSo5IlrLaSc16-v-I-YnS0nRhB8Xs96Cs1aaYOGRLfDT8g-FFTgulVVetpXmhexPP84VuxH8gX6Mt_K4cv_um43PkWi9-rl6MEOdFocr8ZQpVplEm9D7nnpA3mheZQ==$zdeyUubo__F6YG5SBl1YyULx_tcAs1Ehe0u9X51xyCc=", 55 | "decrypted": "this is a test message" 56 | }, 57 | { 58 | "sender": "wolf", 59 | "recipient": "wolf", 60 | "encrypted": "dhole100VJxb4C1L8VejAGWxnmIBlipOzAUV21YRUDzEdwPMXbPikgBrwnsCgol3mIbwvYLB3AfvsA0zVLcv-2bliI8W4LtdXTDwGjcVs0ZjJQmLoFr_l18dlQeS1M_kCmL4RWcr2eCZitjaEckkQ1x44LNV2E9PBkHWJ2KxS9oYx8AuF57wPQXi_kD6XiDcRbr0uMbMzv6cU1ZwcohqEbx8GNbC0hHylfsVXUGIoZtZm5bof0qyVjgdbs0HoeV6xKaohQ==$Ntd8m9CbizKTzXaIR0OwI7E8UQ-tY9zr4nemBXw6YXc=", 61 | "decrypted": "this is a test message" 62 | }, 63 | { 64 | "sender": "wolf", 65 | "recipient": "fox", 66 | "encrypted": "dhole100JFsEz5hTtJLXY2VujIlSRn-DpxvTiJEbnRxlDXhxcDO7xzSmLjjrDSMUpsX2Pvw25X0SlSPLDKmrHpIwMrHwN-C3hVE7NnVRgh9OpeHErYNhugGt7rfRTYVhuNocNuAVX9QL7iEQ84GZ_HvNrnFIuI5h0Lc96J_8b5sVDYVfEiTRzq9BiIaFWYFwEu7kBaZwlYTIBDFWAUaIvwjV8DR6Xr6k1Gt22tDW8BkvRfGynvXRThmZtCokyjzPugGlwAXQ6_aMUeo=$yEI_3Wg66ZADKMukaDg4zaOCeUZ4xnIiX6ow1Suvpgs=", 67 | "decrypted": "trans rights are human rights" 68 | }, 69 | { 70 | "sender": "fox", 71 | "recipient": "wolf", 72 | "encrypted": "dhole100hew6WcSLZXk_5sBQ5jOp8P6p_XpBlGWppZvDITdJZRtlF4xveIy7PqOr4Sm0XulBUy6CtgTX6pZtuE9N6g_aegtukItxfCtTAma_tSnH87OmViQhc058OPp4lPzlsOwoOhV9T5IrUumnbIK8qCNMaU1iQB_dZ38w8S0WWh5ewEyuJ-MT3arGlptYbkpT_XpLRl56iPVFwD7UuB20mPnNgCTTMDIIxL1dgSAfNXqIrraut_rWzpDuKsA6ZepzMV4lxayGB1g=$yon0sUqDscn9aVroZ3GKRjDJqDvk5SUqQsYb-JivcFU=", 73 | "decrypted": "trans rights are human rights" 74 | }, 75 | { 76 | "sender": "fox", 77 | "recipient": "fox", 78 | "encrypted": "dhole100_zqH4ynqn-3WGgilsXHb-wcy63_uToxrmw_X0P6DSY2SyK519eE9dgl_vj1a33z3qfLR3G4CCzbMdtxIbQnpJQAZENkTViHqsUwNmaBikzHRxSMeDcFx8Rum9WvtUUFQVEPSQ7vhglIDwsaX4C6UUPSsbSx1ru3w0jMvRc4azXN7uJONHqMV9l0u_0MnkhX7PhFPELuCbYPwyKcL19rpcpoE8aVkMlBc_k8J7y0WtEZ5F5Yefcd13UEwsiT46tLVJw34dm0=$0AYh9dORpf7GFAlU-J-FAaJxOVhd1seFV1mokJG53wU=", 79 | "decrypted": "trans rights are human rights" 80 | }, 81 | { 82 | "sender": "wolf", 83 | "recipient": "wolf", 84 | "encrypted": "dhole100sOfkK9ab0KYNyZqZj2kjAGUyFH0YkOOwZN-sZdzU9bHejzEr1Hx7zGTUMnucjBxi26Fmpu5B2_VSo7SIp6mTvlpHX4GDYbD5pFDPKxtLP64APnA_86sHM6dQUdpr3xN5AD3WdgrvX3IWDKxN4f3fCINmu3iBBjOveImvyj1fokpoXlbMEPNgkP7mH2YuFj0FkC4T_z2K8PfGMmxzSysP9ChskEPbreYWhWDSGo8QqzHFnl9TvsxH0v5g7K0ZV043c0ZKkuU=$P1QTGJMGDatPu3nK_Wkb4F_oEAwqlQfjRCs36oxrEyI=", 85 | "decrypted": "trans rights are human rights" 86 | }, 87 | { 88 | "sender": "wolf", 89 | "recipient": "fox", 90 | "encrypted": "dhole100jvGpkjbRKhYAXXaCEVc49UXu8DecKxZPT-SdH5KCVwkHuhqqAfmoXbCRtsw739cPmUh-d3jSGD81eI97YDMazRdGZaY5gGIOTaCXgQwwdtkUCtLZxRd5gly4dLbrXITF-0zDB_xZlRjYN1CW_pnDT_VPuAiDQOsB1dKqfZbDfXiuYdcTsWBRfswC8uvt7QYBaKkoeipwRUyoQvx74AvSYiRCGxOqiys-ahnc0pr8j1dRQZtV2pi_O-c-vt0I_d4EijKTBkZinSDn0txBr8DupI4kX4rDkwGQPRvyIF8xqdAH1ek3hivSINTZe6NTEJuyWfF_7bRfNG2Gb1ScU5kHepsvG2hOpm7_SouUVUc5LjXo65gGxvVCth1K5y_43hQpkA==$r_aUgvNRaoB3Ud6OHmno16tCDoOydlNCxQIEnnypJSc=", 91 | "decrypted": "as a furry you are legally obligated to make cringey furry puns whenever pawsible\n\nthere is no excuse fur knot doing so\u0000\n" 92 | }, 93 | { 94 | "sender": "fox", 95 | "recipient": "wolf", 96 | "encrypted": "dhole100Zjtr8y7TLz0up7YwdlB2wjSlGTiuTF6Im-zZDYw6w_qlwE2MBt3SjtO0BHpMk-JJp8lwLvDJHnlK820fhoWDYYosrbl8Wa1xA7G_IoPE0PlE-jdoKs5hSKCF02xlBFPfyJu5gbJSnaYsVoIQWbpu8Ho2_0NrnvMYTyJ_XSvaAyIml4Iv6gdygKSoOGFRfbgGwbgnMz0jB9rb8TUiac8UF-E8EqhMw_yej8uC3JZ6iKErb-59h8aLahxH0hoxm5uw2G7LY_pLDZ2imSooRy9zO693eIYoRblG7Z8j0n-bVzX8lkFowavYikqzo6flLz0oXRZj_T3SDm0jSoWMWu2L5O9bC3ObeXzCvv4NIABMmZ4sOuVbWztGmm5gfshMe3qCoQ==$_53XIzsyUP1CIeI7tqxrFmQk1GNeHZbil-xCzJslsnw=", 97 | "decrypted": "as a furry you are legally obligated to make cringey furry puns whenever pawsible\n\nthere is no excuse fur knot doing so\u0000\n" 98 | }, 99 | { 100 | "sender": "fox", 101 | "recipient": "fox", 102 | "encrypted": "dhole100ybTUTciOKb5KCmiEQhVw-v0d31RzL3AIzMXiSwjsESL3eu-3yoY3MxcIsgxlvTwuNZRi1n8iKI7DpLg3VHMDaM6j-uUiK4L1XTC4-Fe2Uu_L7mqyYuqrrkZKyz8OXBb3RXEBSCS_4HCPxESgf4QeAhxnEZJ9fNJr3N4tcNPIv2uaZGBb7XTgDivpiy6eAoUUnUZgtoTHsaigm4E2cKRSzn0a1Eq_T17D5Qo4JRDdd8j2Ploe6gLWhN9NJ1EpqJ4fa33FAefcjY97wQzGKON0YpmH0GW15cIlHnXBMkjSHacpAH9tAsXyzF5EIavNGlfbrOOol8gUsQgrmBU7FGtHm-7SSNUzGB_Y2GiKoDARgRihCAsbi6MQkP22GaFfV3nQ8g==$jMBftXG2kqXaY27BlTG2MorzmqWQrXbuFiHd44bLh2M=", 103 | "decrypted": "as a furry you are legally obligated to make cringey furry puns whenever pawsible\n\nthere is no excuse fur knot doing so\u0000\n" 104 | }, 105 | { 106 | "sender": "wolf", 107 | "recipient": "wolf", 108 | "encrypted": "dhole100bE1U1KMM2tUb0Wzq58hdAzEwl0TN5vIVJ0EbOOLldVmsZoWZXoolenz2Wvf-kl5xQTsDWgQ-WKa_hTEvIN_x40lN128n0EQOuvjrmBtCoiYiF3npncVnjYjCZeeu9m74glk4mwDVacQBKGmycb945nX-UwaVcBL9dKC9hCq64CAU2SmVsTTBN5TwtfbBydnDjt_RHS1XS65ene9c4uVk7VFyH8c5nNqtSqaAyu9fQ62Qp7uP5N4eziT7EhzdJY4Opck0CAwHsq8ncnxQ3DElQERMrEh-cOyqP7mx_qg1fsFmPUEmL6EvhLGtEFZLcgTrRQiz-kbBmwUHMRejXolqrRhY9rzuV01DbGl8QpovMKJMFxdJUZkWqhCVrLrWgMj1Ug==$PpywmzsbkAO2vgb1KnWAxwnF0USU48bFwq-idG9zFwc=", 109 | "decrypted": "as a furry you are legally obligated to make cringey furry puns whenever pawsible\n\nthere is no excuse fur knot doing so\u0000\n" 110 | } 111 | ], 112 | "seal": [ 113 | { 114 | "recipient": "fox", 115 | "sealed": "dhole100DPM_fwGRVGXBYHwyLfuELzbalEzzrYrfwLt2PWg7eT5sqbwRtLcvsg==$nVR8pvsMIEconJaDdKK10rYdiYbWfsgOMK3RSRiNOTc=", 116 | "unsealed": "" 117 | }, 118 | { 119 | "recipient": "wolf", 120 | "sealed": "dhole100gChrS6cy4PqL9MJvJzHQw-uK71pi3NpecR5W2d8rmImuE2QU5zp2CA==$wb3M6BPN6lLn7qqbi8lnNocvQxL_RT1rvvNvy41kwTA=", 121 | "unsealed": "" 122 | }, 123 | { 124 | "recipient": "fox", 125 | "sealed": "dhole100b9rYEKGUA-NR3RIp7eR6DqTYPXxgkteVKzMbInjS-6G0P-MKt2vG3EYjyDQVwYYyGE21YzPLWsKBY5QicE4=$mzHKhYbMmd-t901whuEjzw25cWp8J0VGGO7x6Sh4e00=", 126 | "unsealed": "this is a test message" 127 | }, 128 | { 129 | "recipient": "wolf", 130 | "sealed": "dhole100tQj5MThwBH8lU7VIXFo-vbK4bxA_3vH4_xOXanQm48Z1lVMk3sDQpwlJnNxmFvyJfW1euwgTrFRF6ZnNLHM=$FtSxUCkWbEvKdgb3znQv-lhHCp16RI8ntdn41p2HwEo=", 131 | "unsealed": "this is a test message" 132 | }, 133 | { 134 | "recipient": "fox", 135 | "sealed": "dhole1006KD_d6RcadA_dCvrT9niZ-Jj-4lIitspdTXoIPGckswMs1ELDuohCWTQCNEqOKKUQo2yRoaibtFzSBBddqaA5E1JOduH$tyKaDu_vqgJonsiJXcPHY1pP1MnbJJ9eMeV7PNJsaV4=", 136 | "unsealed": "trans rights are human rights" 137 | }, 138 | { 139 | "recipient": "wolf", 140 | "sealed": "dhole100FYb_Ktvt_TOqXHJt35xoExcIVqSAoE0Hml1DfCCOtnTJp4Bwcj6yXruDlJtB9SjLxbfM2H6kgizIEXjqbO3POulSXQ12$fD__cSHf9gb6Xh0NbjYVdwTeQXMjdm_glMcEIkRe73Y=", 141 | "unsealed": "trans rights are human rights" 142 | }, 143 | { 144 | "recipient": "fox", 145 | "sealed": "dhole100fpV3ywGGzIJ_nz2lBxNEEWzL0AHNPznrDfKRbvd-98uCL4MQnSJMR_2etd5mX2a4dEwClQ08761CBgS8TbsfntrMF3iYZ_2qLnGmO_hOM-KsxGqZB_mhBb5HDBPMAhIENsniu7Bxlk28Hk4incIWJ5-QX9Ic6wOXX-JJZKK6ptxEp1Um9o2zYCN69XFrUZrBfeQdTMt2EP2Eal3H-BCpzYI=$Z9qFV1bbcNGF-4xTaJLjr5PMYDm0jRi6MwNPhgc5EmE=", 146 | "unsealed": "as a furry you are legally obligated to make cringey furry puns whenever pawsible\n\nthere is no excuse fur knot doing so\u0000\n" 147 | }, 148 | { 149 | "recipient": "wolf", 150 | "sealed": "dhole1003bmQSQSXQ0cZNEjbKMEHImM5PU-PKVO0UtwNUpe7T6_Jr1_a6QVGEKEabH9aVVP5LRoeCabB--DHUBv7cafSjT4iLei068M4ANk38cceouaDTgK4casHk2I_nazzcLM5aurmAqffGH_xbOttz5pnhEqQr97ThsjSGGUxPuTo3OAMFDlTBs89hIBwrrSiHclkydEjwmaSy2GoLPVSLZzvdJQ=$dW7Sy9A8Ug6uGfjSATTJ_qdHfJfQJ5JpRKL4cCBDURQ=", 151 | "unsealed": "as a furry you are legally obligated to make cringey furry puns whenever pawsible\n\nthere is no excuse fur knot doing so\u0000\n" 152 | } 153 | ], 154 | "sign": [ 155 | { 156 | "signer": "fox", 157 | "message": "", 158 | "signature": "NX_Y5vMPtoJMImV_8-CvaYQzqNE7g-s9-umCgC8mLyaM2TS5Mahp_zkZOStk0Ei9ovKZjtFM4p0o5W6lHkfbAGqPvnEA1gGc9Wv2E8CIsG4SfoHbJUBunwjcfvZMn70a" 159 | }, 160 | { 161 | "signer": "wolf", 162 | "message": "", 163 | "signature": "dpT8NfsFf5d67zkcR8fqPyYIj9ZL1HSGobPuHjYcGVSzzUPtjsD7ierbX2R2lQ2UxCz_FZ3Rr_ah5o859l7ACJ9MISq9pZMSmVh2O8zYd0RbCzEknzpC6eyaalfN55E2" 164 | }, 165 | { 166 | "signer": "fox", 167 | "message": "this is a test message", 168 | "signature": "tbVKI7sO030_8A0cVf9g5fSyXu1o93b_-c0YQ6e6PUGP8el0qO-08QiNRpd2Kh7yorrAvFTqyzrjy9HzIgACCp1BusOQpCJDVVNjHHfhUIOrT6O_KqrOK1llIJbdccpI" 169 | }, 170 | { 171 | "signer": "wolf", 172 | "message": "this is a test message", 173 | "signature": "W-kZQi5p_NP268YCjP3B2gzMgyJ7WM-ropMcQhUUpFgPn04i1rhGzFP_1v2bWsYeuc6gMvm54XHb1U17l7J8DZxg7_DI7kPHFVKqI_yNwmvkWdafHA01PPRtOBAKQjQK" 174 | }, 175 | { 176 | "signer": "fox", 177 | "message": "trans rights are human rights", 178 | "signature": "yt3dp5w7xi2Zu21hNaZbVOIlKK6V5eJzwI5gvtrVK1hHeMibWDKIiWYUwm4nTTHE2l3hQLsQoU1jAmcWqjPsAN8Enxj9oj6rphluxA0seJ14DhYIa5lqkoVljQYytMvl" 179 | }, 180 | { 181 | "signer": "wolf", 182 | "message": "trans rights are human rights", 183 | "signature": "UyZ1iTNMioSJX7-Zwd3QnOW5YrF-Yb1k2xXTvtwo8d_qyQDpDNuko6TIk7BC2w_ztzFmdi-OrBy3jAXE9CGuAzpLQ-wQymrHuMopBPkI8TXwNJMMw5zDqdytxVdnYBDM" 184 | }, 185 | { 186 | "signer": "fox", 187 | "message": "as a furry you are legally obligated to make cringey furry puns whenever pawsible\n\nthere is no excuse fur knot doing so\u0000\n", 188 | "signature": "cTLBNHsouxTd06ZfUlW8TvHpMy2B1Y5vDQF-zQ0xgMWQtYrbCZevNFASqk6QurH_hMVZ6hb_eIQA21bZLlm7CowAOE689ztbu2s1SXfcRr5hrdeXz0IcDsDkg1uW9c8r" 189 | }, 190 | { 191 | "signer": "wolf", 192 | "message": "as a furry you are legally obligated to make cringey furry puns whenever pawsible\n\nthere is no excuse fur knot doing so\u0000\n", 193 | "signature": "K7oPKb1SOCnnrqPCqDzQKWLwFiD-hQtA0BL0PIFpiJTNgSWTmX6pqKQ1RV4ksWUGVOmiX2RVM5GBpg7huE6wDLylN0_GbvTtKcNSjUUsQle68tKuPzCft2dUQX1_a5Lm" 194 | } 195 | ] 196 | }, 197 | "asymmetric-file-sign": { 198 | "public-key": "ed25519pkXCR0xjG4e-uo3rr2WxFDACDqgHk2ECamBiv38e1O4s0g3RQm6etECWWacSS7LWn1up4UC90_M0RhwBiRO_fUoBjznlKp1T7PIWs7KpA7FRQ=", 199 | "tests": [ 200 | { 201 | "contents": "", 202 | "signature": "M4JaKWpcYbE5zVpSFrv8az-orR4fG1obZFhBHwgyX7_cBWiX2I9bX2PNKklCQfPguRU6k1mIMj4L25lHv97zCyi-UjP_c2UbrqjTLTovClI-pY9zR5602AWpVBwPjv31" 203 | }, 204 | { 205 | "contents": "this is a test message", 206 | "signature": "hRYfJManWgWejGUcVJAtGTy3JM3IFVXut6tFzZTVaPyRpUXKlXuOL_uzTcaZimkDeAAcBSk1jMPvp4QoRDssBVqY5yDTHfPhB3ujxNTt-m-eFXrll3GO81B-aYjq8jFq" 207 | }, 208 | { 209 | "contents": "trans rights are human rights", 210 | "signature": "xiTSy24t0wC_pz_IkA4GQFCVMcyVejSi6S5nBVW127MtYXMK6BPv0wlKrXlan9XYA_0wGmXc29iMRznPUEBaADItcm2zM6Hi-Dk6Ejl0vYR9UNvE5Mw8nZljmEz1jU6C" 211 | }, 212 | { 213 | "contents": "as a furry you are legally obligated to make cringey furry puns whenever pawsible\n\nthere is no excuse fur knot doing so\u0000\n", 214 | "signature": "9qbpldEPZauMf2bsrh9DYFBoz10_tMA7piMBvIC1aM-JWz9Wi0aHY3raCVE0HFwBVaMz0gdF3vx2DwazlxD8DPR_0C5xVjJmpN_hBRjR1Wsghr6ype6xHvfC7Dm_Goxe" 215 | } 216 | ] 217 | }, 218 | "symmetric": { 219 | "keys": { 220 | "default": "I0_8IIaFzzyCuCoOlpM96k1wr_LXPq5jvorNox5oU4g=", 221 | "fox-to-wolf": "T5c8D4jcrRYKFEU4ooALddyl_cqtxdmjY0DXbaZshAY=", 222 | "wolf-to-fox": "n4x_eppOqnUH8nCGQxoJBvBlovE0iq-p3s58Lfko0hw=", 223 | "fox-from-wolf": "n4x_eppOqnUH8nCGQxoJBvBlovE0iq-p3s58Lfko0hw=", 224 | "wolf-from-fox": "T5c8D4jcrRYKFEU4ooALddyl_cqtxdmjY0DXbaZshAY=", 225 | "key-wrap": "Qgh-5eu2liNWboHIl0xxsaVsuQ0h1-ZgAb7y37J4200=" 226 | }, 227 | "auth": [ 228 | { 229 | "key": "default", 230 | "message": "", 231 | "mac": "62a2c5b780d6f20148f9b0f1ee9885fa5f7a5c70ffe2bbc70de9fa77ab30aab2" 232 | }, 233 | { 234 | "key": "default", 235 | "message": "this is a test message", 236 | "mac": "fe7332521600fb774f16bf97563e31e460933172b8b0b61b8d4dfe1a5e0e972d" 237 | }, 238 | { 239 | "key": "default", 240 | "message": "trans rights are human rights", 241 | "mac": "6292a380bbce03861d3043ab04dc34dc0bae8c6509a4c0eb40911c6c5d130aae" 242 | }, 243 | { 244 | "key": "default", 245 | "message": "as a furry you are legally obligated to make cringey furry puns whenever pawsible\n\nthere is no excuse fur knot doing so\u0000\n", 246 | "mac": "6c8549b360605f1415448bfb2feddbe49bf44b1ba2c1cf455f4006dfedb5909a" 247 | } 248 | ], 249 | "encrypt": [ 250 | { 251 | "key": "default", 252 | "aad": "", 253 | "encrypted": "dhole100zDs8EfXV9Y92f8mGSYQzm6Dc9FBz63Q-oPNXc7xSG5ROcNM34BI-Gw==", 254 | "decrypted": "" 255 | }, 256 | { 257 | "key": "default", 258 | "aad": "Bork whistle", 259 | "encrypted": "dhole100EA98iFHbek0ScyvAGgY2ZKuIMOuq8QqZ7KUzlVVaRbhg2p-YzjXZAA==", 260 | "decrypted": "" 261 | }, 262 | { 263 | "key": "default", 264 | "aad": "", 265 | "encrypted": "dhole100kE99VIRTT2I6guAOoCLBylBfDkC_6VI80g8Xi4PvqLo44Zl6QcLNcO3JIjySrSnlsfCkQw5rMOj4ZBTIfn4=", 266 | "decrypted": "this is a test message" 267 | }, 268 | { 269 | "key": "default", 270 | "aad": "Bork whistle", 271 | "encrypted": "dhole100UliJNF0AeiEzosfTo9HHXgjqpOkEuqzPYUVQ90K0dHW497-FGobujKgGBWI_Qf-uqtmR24zAtaAoyhaJxHg=", 272 | "decrypted": "this is a test message" 273 | }, 274 | { 275 | "key": "default", 276 | "aad": "", 277 | "encrypted": "dhole100RTt07ErEgy8uxnfI4swlIxSK-O6biMpcpNVGUQjOYIDKxjUqSm4hyEVisM4rxwZGPzfpFpaWuRiKtUihbvVwO1OdtdiC", 278 | "decrypted": "trans rights are human rights" 279 | }, 280 | { 281 | "key": "default", 282 | "aad": "Bork whistle", 283 | "encrypted": "dhole100jMUYUsuhUIOfgo6kAfSzCQtKlOGA9jEe0nLaRfLOiKtVL-3Q9Rz9Mn0PpaTJHKcxGxcIwsNGp-vMKwWC6Hgv81dm32-e", 284 | "decrypted": "trans rights are human rights" 285 | }, 286 | { 287 | "key": "default", 288 | "aad": "", 289 | "encrypted": "dhole1009VSkdVgmbRBcvR4EOLVh2ot4P2vg3FK-iiHwmqMpLssw8Erq-4JD5lvRWJgPGIx4rBsYn9Dfk8aFY-CCVQK9MyYh3y_bef45w6XWSDLNRRB3VAWQkfBbEQWr_umnxqHtRA2YjaejrRN7_G3TYmF9uarW6cxHVqTHh5U2xXqdhJDesNM3kwznFG_cj4x69AYgd3SNFAQIQFSsUPkI_Y2GGpg=", 290 | "decrypted": "as a furry you are legally obligated to make cringey furry puns whenever pawsible\n\nthere is no excuse fur knot doing so\u0000\n" 291 | }, 292 | { 293 | "key": "default", 294 | "aad": "Bork whistle", 295 | "encrypted": "dhole1000l2LgGBHP-rtVOm-Re5Q6M3SC08kch8Vnad9GaUh9FfnlGm252l7USmVBZ76lfrXYaPywQ5wuThEHMOI-yBEDcnfZipj_bUPeh-7fCGIHIvOdKPxKe3YASEK15pUIcGgEVc4hJQ1a4dWCbc1sfE4KuECdwmAFl7t0mC9_GJNxPJAC8zTYaEllaO25FpHU0RRgCd8cHAG7seJQuLG4SoZIg4=", 296 | "decrypted": "as a furry you are legally obligated to make cringey furry puns whenever pawsible\n\nthere is no excuse fur knot doing so\u0000\n" 297 | } 298 | ] 299 | }, 300 | "password": { 301 | "valid": [ 302 | { 303 | "key": "default", 304 | "password": "", 305 | "aad": "", 306 | "encrypted-pwhash": "dhole100cZeI7snhl6ko-MoYVBCUVVFE6A4slK_OsOerqfUYrt5IDOsY9wb8GnEmfiZgfawg7SCO-MR27mMeczUXfaQVGxgQntDQ8rbtJGdded7dW_gtMttr8rGHOWrvkgSIgANAOWI67jAe7QdF_-Iyelgt2NSiO8zdubF7iPzo9APF6AeXWOLHrz_2gjk=", 307 | "valid": true 308 | }, 309 | { 310 | "key": "default", 311 | "password": "", 312 | "aad": "Bork whistle", 313 | "encrypted-pwhash": "dhole100RBQGAUdBdWbK24siigqB6AfSW8Qr8b4ph_fppa9rymSvyQPZgugdDOXsUWAqIMRM1EnzUgHxJYfD3XCt_Cc5kDyD4-AY6-OvnL8KSllyzq1qnf_LD6YOZvVmz-2ZJNf19tIkk75rd_Tdd23mr3gUq5F2HCPbseVFBeZ35rd1H01gwWOWGL8YdII=", 314 | "valid": true 315 | }, 316 | { 317 | "key": "default", 318 | "password": "this is a test message", 319 | "aad": "", 320 | "encrypted-pwhash": "dhole100Cv7Nw9As-g_FA2fjPUknQIf-AD3V_wYD_u0X2_xGqexlr_eV9IqC4Iq-rgYXhBpNhAUbas6BxGiCiug8EIbHZbWIubpOp1HGzdFKzFrIBrNnzpaaZ1jtVVirIiHEaq3aZV7ICHaJxkOYcgaMfI2swjpgoP5UvGVQkuE95oTYWifPzpgw2JG2GJw=", 321 | "valid": true 322 | }, 323 | { 324 | "key": "default", 325 | "password": "this is a test message", 326 | "aad": "Bork whistle", 327 | "encrypted-pwhash": "dhole100UgVPWK7Q2hc0CVF2gRgCuhIea79zsRcUZ6Vt7savRWbCHzMHQJgCPfvreRkbi2nNnw1Mv91vQALZeGGCuUSQ77xSXk_c7CqZX5fOzpbmNzQVwkoBgz6euWK6oGb69afHYShkJyiNrxE1-9xovL3jtE_dsgI8DVeRccGbJ5SH3T0oCiB_JjJ3UWs=", 328 | "valid": true 329 | }, 330 | { 331 | "key": "default", 332 | "password": "trans rights are human rights", 333 | "aad": "", 334 | "encrypted-pwhash": "dhole100qNoUU9mOyri5UYUsq1BGjLanB1aYlpuqsUJZhxXg38y2tHIQ0Zm8y2cMaBU8867c0hLW6Idm7gFoFGKCUYRckLQZPFa8ILmXO6o7gm7Q8-gmL1XOgEayW1zKZZk2Pjq7KW5jiOf4H538lasjGhZ64Iip_3PFKxbVS6--k7e3vyuleEjUnW2CwGI=", 335 | "valid": true 336 | }, 337 | { 338 | "key": "default", 339 | "password": "trans rights are human rights", 340 | "aad": "Bork whistle", 341 | "encrypted-pwhash": "dhole100O0q4qetOY8SM1-P2rFMvx_VYiJ5B4dPmc2JD-fvVdeQpILvV-6YPKn_-effvnByc_IRVrDIdUHzAiLLKlFa9IcpoD98HaXY5Suvi8JwGZo66NkIQGGXuFmg_CKMJT0J0s5gdyiFcQWvcKjM-Gkn1ofcM0Nt1YEUvDsI7ovl9AS83-qf0Ipo8bsU=", 342 | "valid": true 343 | }, 344 | { 345 | "key": "default", 346 | "password": "as a furry you are legally obligated to make cringey furry puns whenever pawsible\n\nthere is no excuse fur knot doing so\u0000\n", 347 | "aad": "", 348 | "encrypted-pwhash": "dhole100SZTjYU9J0EjF0fxMZ598skELlDJhIHNRG9UPSd-CZhlBjfKlMYRNP8CRU0UJ0HIRz0vsF5WeTsZfSiQvFU9D2ExddROqbTPOEcZqIgGvcYfAKhBjmxpDcLogpxfP2a8mgDEwHirLmB0i4Zio1mpxxLghZrZRL3bL9QicHA-4Ehx1E5gFrvVSAe4=", 349 | "valid": true 350 | }, 351 | { 352 | "key": "default", 353 | "password": "as a furry you are legally obligated to make cringey furry puns whenever pawsible\n\nthere is no excuse fur knot doing so\u0000\n", 354 | "aad": "Bork whistle", 355 | "encrypted-pwhash": "dhole1004JGgYosI50qwOAjIOIyQ-x7-nBmEDScgjFd5TwEocMLhnGrIzz5q5skQfsCXr7ASCIpfeQNJDIzuBFPUxxbZmqDp6oH3Adpvy3ubuPjF3njyvAU02BpTBkCsW3L4AbjUBPRVUCWXKSa7QFrJCv_TGYFCS8EAm3-S930nJWHX8xRhPAto_DG52e0=", 356 | "valid": true 357 | } 358 | ] 359 | }, 360 | "key-ring": { 361 | "non-wrapped": { 362 | "fox-secret-key": "ed25519sk2R_Q9l3kLs2bk3okM5CXdBCPIjna3zbFrfQlhveMXv6AMWuTBUCAOitbLwcA6zuGIOqAeTYQJqYGK_fx7U7izSDdFCbp60QJZZpxJLstafUw7ErlW_rsgQcKKAGZhnOtr15nfc9eFKJt_wIgWigKdQ==", 363 | "fox-public-key": "ed25519pkXCR0xjG4e-uo3rr2WxFDACDqgHk2ECamBiv38e1O4s0g3RQm6etECWWacSS7LWn1up4UC90_M0RhwBiRO_fUoBjznlKp1T7PIWs7KpA7FRQ=", 364 | "wolf-secret-key": "ed25519sk6Ja4qbtNkJ4-C8drfwNSHJhkEo1NuL9JCsOEERTOG3xe2kv7gm4JbbWKf4HX9QcWV0JIXcOB0GLAQazSNx6G_113SST6fMfiGT7Z3PlGmudInIn25SFVGqVwQd-5tqnGlB0mzSOdB8aKrlXXHvU6fw==", 365 | "wolf-public-key": "ed25519pkpAWsiPL2kDyhYM-6ffx4XVdCSF3DgdBiwEGs0jcehv9dd0kk-nzH4hk-2dz5Rprn-0wkWqfjDOerfRLOvHplSwOBIVoCWZjA7ov4ejZOOmk=", 366 | "symmetric-default": "symmetricu_yqfKWR8jgJQtqSwueWvSNP_CCGhc88grgqDpaTPepNcK_y1z6uY76KzaMeaFOI" 367 | }, 368 | "wrapped": { 369 | "fox-secret-key": "dhole100aOvN7vaTUoZvzBEQaysrDP0YLuIyWAb0MNbAc223NUE2b4GH5k3HnQL96i6rVqpNCgiAV0Oz-F1gkK41yZ1PVRXeaiPJQm216ooMerzRITCMvnMbYYRySGLdLYWh96N1LpXvuek0q5U_6c0aq6iRFVfm9sxaZ5T89cyXshGFJpfLwBtPQcvGTeiFo8EQJVe8UwkeaK65QMoWGfBQJpCUJmqbSL_l3wevu9NG8SLyPiCTvG1x8SYCKd3Pu9wUrRxZhg1Q6aEEBuUF", 370 | "fox-public-key": "dhole100SL-x8RgzmxY5loXlx2-O88rESVyJmOqUWnLXm-W-kSU0uqzSE9Q1wgbaTdzK-BwqUB8dlIXjsZGLYwZnJzKjb-FhvFRFW4vyeUKyqDebAAgKL1E7Ql2pf4WEpUwmIAVJLWW2y4ni5agfHrVtT72shnmkuXJCEIMGUv_yFBTNLhcd7YKa54YFE8Q0TD9b3W_ZYlOhQjRbzkhnUNVcdw==", 371 | "wolf-secret-key": "dhole100FS97FuByqR1ObXMv1q_s92c7VSLu3SVH0VNTPvq0_zkvk7G-HEOGNCACczWEnsgLZzfiHPNtxUwcqfmo9LHNk-RurBddeChbuQNCHNYaKRfcALyKiIfcw_JT5xPT8WeMIMUs5wwyFvtk0To0AoPQl8w_y99HTxgEABiLV6w0tdsHYCmIPudr-96ydHt_LikBT1u63dqDWM4KgJq20St7uaF8GWl_tENcth5r-niKsKze8Bx3djSEjbI_Z6GUbglMD84Gz5Niy6Sg", 372 | "wolf-public-key": "dhole100RZhtUm6d8U9wXK-yfdhDBwiFYvQGoV4Qn9D8K8_yPlCzq5DSIA3yz2Ky-vn49jptw0oVtuDl7jdqwvDu9ZG4xbGbNS7lUUh9xmbTp5uYskPwePRa7U8R1EjnJyz0CwnTlwz9tveK94vDYiLXc0Uay4wqJeOnW0nzt-Cw-SnQGWMl8y0EH584K3fTptZiZ4NeYlXyULFUgoiBfP4glA==", 373 | "symmetric-default": "dhole1006-mStd1FpIIxxwYl68XJdGwlf-7W-oIWTvf1B0yAh64gQxALzybggJcqaSyp594tc68aolZh9Wqo4F8t27vn2kxs40RLxHP1PTFARXpZ6WuXRaHtQ7kYhwOQuoCWfjO4wodNjNKjGqNX4dKTXNl_sQo=" 374 | } 375 | } 376 | } --------------------------------------------------------------------------------