GithubHelp home page GithubHelp logo

secrets.js's Introduction

secrets.js

NOTICE: This project is no longer maintained.

Take a look at https://github.com/grempe/secrets.js for many improvements and enhancements on my original project.


See it in action at http://passguardian.com

What is it?

secrets.js is an implementation of Shamir's threshold secret sharing scheme in javascript, for node.js and browsers.

It can be used to split any "secret" (i.e. a password, text file, Bitcoin private key, anything) into n number of "shares" (each the same size in bits as the original secret), requiring that exactly any number t ("threshold") of them be present to reconstruct the original secret.

Examples:

Divide a 512-bit key, expressed in hexadecimal form, into 10 shares, requiring that any 5 of them are necessary to reconstruct the original key:

// generate a 512-bit key
var key = secrets.random(512); // => key is a hex string

// split into 10 shares with a threshold of 5
var shares = secrets.share(key, 10, 5); 
// => shares = ['801xxx...xxx','802xxx...xxx','803xxx...xxx','804xxx...xxx','805xxx...xxx']

// combine 4 shares
var comb = secrets.combine( shares.slice(0,4) );
console.log(comb === key); // => false

// combine 5 shares
var comb = secrets.combine( shares.slice(4,9) );
console.log(comb === key); // => true

// combine ALL shares
var comb = secrets.combine( shares );
console.log(comb === key); // => true

// create another share with id 8
var newShare = secrets.newShare(8, shares); // => newShare = '808xxx...xxx'

// reconstruct using 4 original shares and the new share:
var comb = secrets.combine( shares.slice(1,5).concat(newShare) );
console.log(comb === key); // => true

Divide a password containing a mix of numbers, letters, and other characters, requiring that any 3 shares must be present to reconstruct the original password:

var pw = '<<PassWord123>>';

// convert the text into a hex string
var pwHex = secrets.str2hex(pw); // => hex string

// split into 5 shares, with a threshold of 3
var shares = secrets.share(pwHex, 5, 3);


// combine 2 shares:
var comb = secrets.combine( shares.slice(1,3) );

//convert back to UTF string:
comb = secrets.hex2str(comb);
console.log( comb === pw  ); // => false


// combine 3 shares:
var comb = secrets.combine( [ shares[1], shares[3], shares[4] ] );

//convert back to UTF string:
comb = secrets.hex2str(comb);

console.log( comb === pw  ); // => true

Installation and usage

secrets.js is available on npm. Install using

npm install secrets.js

The version on npm may not always reflect all the latest changes, especially during times of heavy development. To get the most up-to-date version, download the current master zip file, then run the following code from inside the folder:

npm install

To use it in node.js:

var secrets = require('secrets.js');

To use it in the browser, include secrets.js or secrets.min.js (minified using Google Closure Compiler)

<script src="secrets.min.js"></script>

API

  • secrets.share()
  • secrets.combine()
  • secrets.newShare()
  • secrets.init()
  • secrets.getConfig()
  • secrets.setRNG()
  • secrets.random()
  • secrets.str2hex()
  • secrets.hex2str()

secrets.share( secret, numShares, threshold, [padLength] )

Divide a secret expressed in hexadecimal form into numShares number of shares, requiring that threshold number of shares be present for reconstructing the secret;

  • secret: String, required: A hexadecimal string.
  • numShares: Number, required: The number of shares to compute. This must be an integer between 2 and 2^bits-1 (see secrets.init() below for explanation of bits).
  • threshold: Number, required: The number of shares required to reconstruct the secret. This must be an integer between 2 and 2^bits-1 (see secrets.init() below for explanation of bits).
  • padLength: Number, optional, default 0: How much to zero-pad the binary representation of secret. This ensures a minimum length for each share. See "Note on security" below.

The output of secrets.share() is an Array of length numShares. Each item in the array is a String. See Share format below for information on the format.

secrets.combine( shares )

Reconstructs a secret from shares.

  • shares: Array, required: An Array of shares. The form is equivalent to the output from secrets.share().

The output of secrets.combine() is a String representing the reconstructed secret. Note that this function will ALWAYS produce an output String. However, if the number of shares that are provided is not the threshold number of shares, the output will not be the original secret. In order to guarantee that the original secret is reconstructed, the correct threshold number of shares must be provided.

Note that using more than the threshold number of shares will also result in an accurate reconstruction of the secret. However, using more shares adds to computation time.

secrets.newShare( id, shares )

Create a new share from the input shares.

  • id: Number or String, required: A Number representing the share id. The id is an integer between 1 and 2^bits-1. It can be entered as a Number or a number String expressed in hexadecimal form.
  • shares: Array, required: The array of shares (in the same format as outputted from secrets.share()) that can be used to reconstruct the original secret.

The output of secrets.newShare() is a String. This is the same format for the share that secrets.share() outputs. Note that this function ALWAYS produces an output String. However, as for secrets.combine(), if the number of shares that are entered is not the threshold number of shares, the output share will not be a valid share (i.e. will not be useful in reconstructing the original secret). In order to guarantee that the share is valid, the correct threshold number of shares must be provided.

secrets.init( [bits] )

Set the number of bits to use for finite field arithmetic.

  • bits: Number, optional, default 8: An integer between 3 and 20. The number of bits to use for the Galois field.

Internally, secrets.js uses finite field arithmetic in binary Galois Fields of size 2^bits. Multiplication is implemented by the means of log and exponential tables. Before any arithmetic is performed, the log and exp tables are pre-computed. Each table contains 2^bits entries.

bits is the limiting factor on numShares and threshold. The maximum number of shares possible for a particular bits is (2^bits)-1 (the zeroth share cannot be used as it is the secret by definition.). By default, secrets.js uses 8 bits, for a total 2^8-1 = 255 possible number of shares. To compute more shares, a larger field must be used. To compute the number of bits you will need for your numShares or threshold, compute the log-base2 of (numShares+1) and round up, i.e. in javascript: Math.ceil(Math.log(numShares+1)/Math.LN2).

Note:

  • secrets.init() does NOT need to be called if you plan on using the default of 8 bits. It is automatically called on loading the library.
  • The size of the exp and log tables depends on bits (each has 2^bits entries). Therefore, using a large number of bits will cause a slightly longer delay to compute the tables.
  • The theoretical maximum number of bits is 31, as javascript performs bitwise operations on 31-bit numbers. A limit of 20 bits has been hard-coded into secrets.js, which can produce 1,048,575 shares. secrets.js has not been tested with this many shares, and it is not advisable to go this high, as it may be too slow to be of any practical use.
  • The Galois Field may be re-initialized to a new setting when secrets.newShare() or secrets.combine() are called with shares that are from a different Galois Field than the currently initialized one. For this reason, use secrets.getConfig() (see below) to check what the current bit-setting is.

secrets.getConfig()

Returns an Object with the current configuration. Has the following properties:

  • bits: [Number] The number of bits used for the current initialized finite field
  • unsafePRNG: [Boolean]: Is true when Math.random() is being used as the PRNG

secrets.setRNG( function(bits){} )

Set the pseudo-random number generator used to compute shares.

secrets.js uses a PRNG in the secrets.share() and secrets.random() functions. By default, it tries to use a cryptographically strong PRNG. In node.js this is crypto.randomBytes(). In browsers that support it, it is crypto.getRandomValues() (using typed arrays, which must be supported too). If neither of these are available it defaults to using Math.random(), which is NOT cryptographically strong (except reportedly in Safari, though I have yet to confirm this). A warning will be displayed in the console and in an alert box in browsers when Math.random() is being used.

To supply your own PRNG, use secrets.setRNG(). It expects a Function of the form function(bits){}. It should compute a random integer between 1 and 2^bits-1. The output must be a String of length bits containing random 1's and 0's (cannot be ALL 0's). When secrets.setRNG() is called, it tries to check the PRNG to make sure it complies with some of these demands, but obviously it's not possible to run through all possible outputs. So make sure that it works correctly.

If you are just planning on using secrets.combine() or secrets.newShare(), then no PRNG is required. It is only used by the secrets.share() and secrets.random() functions.

NOTE: In a near-future version of secrets.js, the requirement for the PRNG function will be less convoluted. It probably will just have to return a random integer between 1 and max, or something like that.

secrets.random( bits )

Generate a random bits-length string, and output it in hexadecimal format. bits must be an integer greater than 1.

secrets.str2hex( str, [bytesPerChar] )

Convert a UTF string str into a hexadecimal string, using bytesPerChar bytes (octets) for each character.

  • str: String, required: A UTF string.
  • bytesPerChar: Number, optional, default 2. The maximum bytesPerChar is 6 to ensure that each character is represented by a number that is below javascript's 2^53 maximum for integers.

secrets.hex2str( str, [bytesPerChar] )

Convert a hexadecimal string into a UTF string. Each character of the output string is represented by bytesPerChar bytes in the String str. See note on bytesPerChar under secrets.str2hex() above.

Share format

Each share is a string in the format <bits><id><value>. Each part of the string is described below:

  • bits: The first character, expressed in base-36 format, is the number of bits used for the Galois Field. This number must be between 3 and 20, expressed by the characters [3-9, a-k].
  • id: The id of the share. This is a number between 1 and 2^bits-1, expressed in hexadecimal form. The number of characters used to represent the id is the character-length of the representation of the maximum id (2^bits-1) in hexadecimal: (Math.pow(2,bits)-1).toString(16).length.
  • value: The value of the share, expressed in hexadecimal form. The length of this string depends on the length of the secret.

Note on security

Shamir's secret sharing scheme is "information-theoretically secure" and "perfectly secure" in that less than the requisite number of shares provide no information about the secret (i.e. knowing less than the requisite number of shares is the same as knowing none of the shares). However, because the size of each share is the same as the size of the secret (when using binary Galois fields, as secrets.js does), in practice it does leak some information, namely the size of the secret. Therefore, if you will be using secrets.js to share short password strings (which can be brute-forced much more easily than longer ones), it would be wise to zero-pad them so that the shares do not leak information about the size of the secret.

When secrets.share() is called with a padLength, the secret is zero-padded so that it's length is a multiple of the padLength. The second example above can be modified to use 1024-bit zero-padding, producing longer shares:

var pw = '<<PassWord123>>';

// convert the text into a hex string
var pwHex = secrets.str2hex(pw); // => 240-bit password

// split into 5 shares, with a threshold of 3, WITH zero-padding
var shares = secrets.share(pwHex, 5, 3, 1024); // => 1024-bit shares

// combine 3 shares
var comb = secrets.combine( [ shares[1], shares[3], shares[4] ] );

// convert back to UTF string
comb = secrets.hex2str(comb);

console.log( comb === pw  ); // => true

License

secrets.js is released under the MIT License. See LICENSE.

Changelog

  • 0.1.8: bugfix release
  • 0.1.7: added config.unsafePRNG reset when supplying a new PRNG
  • 0.1.6:
    • Removed JSBN dependency, support for arbitrary radices, and the convertBase() function, with attendant 50% file size reduction.
    • Fixed bug where leading zeros were dropped.
    • Renamed string conversion functions.
  • 0.1.5: getConfig() returns information about PRNG
  • 0.1.4: new share format

Possible future enhancements

secrets.js's People

Contributors

amper5and avatar robertbaron avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

secrets.js's Issues

Error: Invalid Share when running from console

From the console, newShare() throws the following error:

secrets.newShare(33, "<secret share>", "<secret share>")
Error: Invalid share : Share id must be an integer between 1 and 255, inclusive.

Uh. Doesn't actually appear to be SSS

This looks like it's just using a radix-16 RS code?

If so, then I don't think it's information theoretically secure. Classic SSS fixes the whole message in a single field element.

I think just doing an RS code over bytes/shorts results in sub-threshold shares leaking data about linear relationships between different words in the secret.

Do you have a citation for the approach that you're using?

Share hex length is (I think) always invalid (odd)

The issue looks to be with the prefixing, which always appends an "8" (https://github.com/amper5and/secrets.js/blob/master/secrets.js#L246)

From what I can see, the following lines aren't quite right:

for(var i=0; i<numShares; i++){
    x[i] = config.bits.toString(36).toUpperCase() + padLeft(x[i],padding) + bin2hex(y[i]);
}
  1. Why are we using base 36? For a larger number than 8, (ie, 200), there's no way that this will generate a valid hexadecimal value... ((200).toString(36) == '5k')

  2. Assuming we meant this to be 16 instead of 36 (which makes much more sense), it still can result in an odd-length string, which is 👎 ((8).toString(16) == '8')

    • To illustrate why this is a bad time, here's a traceback:
    > new Buffer((8).toString(16), 'hex');
    TypeError: Invalid hex string
    at TypeError (native)
    at Buffer.write (buffer.js:568:21)
    at fromString (buffer.js:115:26)
    at new Buffer (buffer.js:54:12)
    at repl:1:1
    at REPLServer.defaultEval (repl.js:248:27)
    at bound (domain.js:287:14)
    at REPLServer.runBound [as eval] (domain.js:300:12)
    at REPLServer.<anonymous> (repl.js:412:12)
    at emitOne (events.js:82:20)

I think we should do what you did a bit later in the line with padLeft:

for(var i = 0; i < numShares; i++) {
    // x[i] is (even-length number of bits, any padding requested, and then the secret share)
    var prefix = padLeft(config.bits.toString(16).toUpperCase(), 2),
        padding = padLeft(x[i], padding);
    x[i] = prefix + padding + bin2hex(y[i]);
}

The question then becomes, what corresponding changes do we need for the combining portion of code...?

I think we would need to update the processShare method to make this work (https://github.com/amper5and/secrets.js/blob/master/secrets.js#L301)

(the lines affected are var bits = ... and var id = ...)

function processShare(share){
    var bits = parseInt(share.substring(0, 2), 16);
    if(bits && (typeof bits !== 'number' || bits%1 !== 0 || bits<defaults.minBits || bits>defaults.maxBits)){
        throw new Error('Number of bits must be an integer between ' + defaults.minBits + ' and ' + defaults.maxBits + ', inclusive.')
    }

    var max = Math.pow(2, bits) - 1;
    var idLength = max.toString(config.radix).length;

    var id = parseInt(share.substr(2, idLength), config.radix);
    if(typeof id !== 'number' || id%1 !== 0 || id<1 || id>max){
        throw new Error('Share id must be an integer between 1 and ' + config.max + ', inclusive.');
    }
    share = share.substr(idLength + 1);
    if(!share.length){
        throw new Error('Invalid share: zero-length share.')
    }
    return {
        'bits': bits,
        'id': id,
        'value': share
    };
};

It is possible to select an incorrect configuration

The UI lets one split a secret into 3 shares with a threshold of 4. Even if the user should not happen, this is problematic as even if the user keeps all the shares they may lose the original data.

For example, asking for 3 shares with a threshold of 4 for the string "This is a test" yielded

8013171e9f3439af6dd2875d1c5b8a7f278c9de4b27fc0be977f59dbeadf0
80270671046eab48a3ff8190988d7242964cf2450c9c849d40b26b6446636
8036ae31f18f5afa8f5bcd903ed599f0ca79c9d3de9af5a1d0d035e9e279d

but decoded it as "畤燐ᠠޛ朦뮚᳗ꀶ뗛ᝬ臔굜�".

Since there is a non-negligible risk of data loss due to an operator error, the UI should refuse to encode the data if the threshold is greater than the number of shares.

base58 encode/decode

Would it be possible to add a base58 encode/decode option?

Here's an implementation of the algorithm.
https://gist.github.com/inflammable/2929362

I'm trying to use PassGuardian to split a bitcoin wallet key, and I'd like the resulting output to be as short as possible. Right now I could still do this myself, taking the base58 key, decoding it to a number, splitting it with the secrets lib, and then base58 encoding the pieces.

However I'd like someone less computer savvy than myself to be able to reassemble the key if I'm not around.

I'll put up a $20 bounty (in bitcoins). I know that is probably not enough to cover the effort but maybe others will contribute.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.