├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── analysis_options.yaml ├── lib ├── password_hash.dart ├── pbkdf2.dart └── salt.dart ├── pubspec.yaml └── test └── pbkdf2_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/tools/private-files.html 2 | 3 | # Files and directories created by pub 4 | .packages 5 | .pub/ 6 | build/ 7 | # If you're building an application, you may want to check-in your pubspec.lock 8 | pubspec.lock 9 | 10 | # Directory created by dartdoc 11 | # If you don't generate documentation locally you can remove this line. 12 | doc/api/ 13 | .idea/ 14 | 15 | .dart_tool/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: dart 2 | dart: 3 | - stable 4 | 5 | jobs: 6 | include: 7 | - stage: test 8 | script: pub get && pub run test 9 | 10 | stages: 11 | - test 12 | 13 | branches: 14 | only: 15 | - master 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 stable/kernel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # password_hash 2 | 3 | [![Build Status](https://travis-ci.org/stablekernel/dart-password-hash.svg?branch=master)](https://travis-ci.org/stablekernel/dart-password-hash) 4 | 5 | Implements PBKDF2 algorithm for securely hashing passwords. 6 | 7 | Usage: 8 | 9 | ``` 10 | var generator = new PBKDF2(); 11 | var salt = Salt.generateAsBase64String(); 12 | var hash = generator.generateKey("mytopsecretpassword", salt, 1000, 32); 13 | ``` 14 | 15 | 16 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | strong-mode: true 3 | 4 | linter: 5 | rules: 6 | - camel_case_types 7 | - empty_constructor_bodies 8 | - always_declare_return_types 9 | - camel_case_types 10 | - constant_identifier_names 11 | - empty_constructor_bodies 12 | - implementation_imports 13 | - library_names 14 | - library_prefixes 15 | - non_constant_identifier_names 16 | - one_member_abstracts 17 | - package_api_docs 18 | - package_prefixed_library_names 19 | - prefer_is_not_empty 20 | - slash_for_doc_comments 21 | - super_goes_last 22 | - type_annotate_public_apis 23 | - type_init_formals 24 | - unnecessary_getters_setters 25 | - avoid_empty_else 26 | - package_names 27 | - unrelated_type_equality_checks 28 | - throw_in_finally 29 | - close_sinks 30 | - comment_references 31 | - control_flow_in_finally 32 | - empty_statements 33 | - hash_and_equals 34 | - iterable_contains_unrelated_type 35 | - list_remove_unrelated_type 36 | - test_types_in_equals 37 | - throw_in_finally 38 | - valid_regexps 39 | - annotate_overrides 40 | - avoid_init_to_null 41 | - avoid_return_types_on_setters 42 | - await_only_futures 43 | - empty_catches 44 | - prefer_is_not_empty 45 | - sort_constructors_first 46 | - sort_unnamed_constructors_first 47 | - unawaited_futures 48 | -------------------------------------------------------------------------------- /lib/password_hash.dart: -------------------------------------------------------------------------------- 1 | export 'salt.dart'; 2 | export 'pbkdf2.dart'; -------------------------------------------------------------------------------- /lib/pbkdf2.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | import 'dart:math'; 4 | 5 | import 'package:crypto/crypto.dart'; 6 | 7 | import 'package:password_hash/salt.dart'; 8 | 9 | /// Instances of this type derive a key from a password, salt, and hash function. 10 | /// 11 | /// https://en.wikipedia.org/wiki/PBKDF2 12 | class PBKDF2 { 13 | /// Creates instance capable of generating a key. 14 | /// 15 | /// [hashAlgorithm] defaults to [sha256]. 16 | PBKDF2({Hash hashAlgorithm}) { 17 | this.hashAlgorithm = hashAlgorithm ?? sha256; 18 | } 19 | 20 | Hash get hashAlgorithm => _hashAlgorithm; 21 | set hashAlgorithm(Hash algorithm) { 22 | _hashAlgorithm = algorithm; 23 | _blockSize = _hashAlgorithm.convert([1, 2, 3]).bytes.length; 24 | } 25 | 26 | Hash _hashAlgorithm; 27 | int _blockSize; 28 | 29 | /// Hashes a [password] with a given [salt]. 30 | /// 31 | /// The length of this return value will be [keyLength]. 32 | /// 33 | /// See [Salt.generateAsBase64String] for generating a random salt. 34 | /// 35 | /// See also [generateBase64Key], which base64 encodes the key returned from this method for storage. 36 | List generateKey( 37 | String password, String salt, int rounds, int keyLength) { 38 | if (keyLength > (pow(2, 32) - 1) * _blockSize) { 39 | throw new PBKDF2Exception("Derived key too long"); 40 | } 41 | 42 | var numberOfBlocks = (keyLength / _blockSize).ceil(); 43 | var hmac = new Hmac(hashAlgorithm, utf8.encode(password)); 44 | var key = new ByteData(keyLength); 45 | var offset = 0; 46 | 47 | var saltBytes = utf8.encode(salt); 48 | var saltLength = saltBytes.length; 49 | var inputBuffer = new ByteData(saltBytes.length + 4) 50 | ..buffer.asUint8List().setRange(0, saltBytes.length, saltBytes); 51 | 52 | for (var blockNumber = 1; blockNumber <= numberOfBlocks; blockNumber++) { 53 | inputBuffer.setUint8(saltLength, blockNumber >> 24); 54 | inputBuffer.setUint8(saltLength + 1, blockNumber >> 16); 55 | inputBuffer.setUint8(saltLength + 2, blockNumber >> 8); 56 | inputBuffer.setUint8(saltLength + 3, blockNumber); 57 | 58 | var block = _XORDigestSink.generate(inputBuffer, hmac, rounds); 59 | var blockLength = _blockSize; 60 | if (offset + blockLength > keyLength) { 61 | blockLength = keyLength - offset; 62 | } 63 | key.buffer.asUint8List().setRange(offset, offset + blockLength, block); 64 | 65 | offset += blockLength; 66 | } 67 | 68 | return key.buffer.asUint8List(); 69 | } 70 | 71 | /// Hashed a [password] with a given [salt] and base64 encodes the result. 72 | /// 73 | /// This method invokes [generateKey] and base64 encodes the result. 74 | String generateBase64Key( 75 | String password, String salt, int rounds, int keyLength) { 76 | var converter = new Base64Encoder(); 77 | 78 | return converter.convert(generateKey(password, salt, rounds, keyLength)); 79 | } 80 | } 81 | 82 | /// Thrown when [PBKDF2] throws an exception. 83 | class PBKDF2Exception implements Exception { 84 | PBKDF2Exception(this.message); 85 | String message; 86 | 87 | @override 88 | String toString() => "PBKDF2Exception: $message"; 89 | } 90 | 91 | class _XORDigestSink extends Sink { 92 | _XORDigestSink(ByteData inputBuffer, Hmac hmac) { 93 | lastDigest = hmac.convert(inputBuffer.buffer.asUint8List()).bytes; 94 | bytes = new ByteData(lastDigest.length) 95 | ..buffer.asUint8List().setRange(0, lastDigest.length, lastDigest); 96 | } 97 | 98 | static Uint8List generate(ByteData inputBuffer, Hmac hmac, int rounds) { 99 | var hashSink = new _XORDigestSink(inputBuffer, hmac); 100 | 101 | // If rounds == 1, we have already run the first hash in the constructor 102 | // so this loop won't run. 103 | for (var round = 1; round < rounds; round++) { 104 | var hmacSink = hmac.startChunkedConversion(hashSink); 105 | hmacSink.add(hashSink.lastDigest); 106 | hmacSink.close(); 107 | } 108 | 109 | return hashSink.bytes.buffer.asUint8List(); 110 | } 111 | 112 | ByteData bytes; 113 | Uint8List lastDigest; 114 | 115 | @override 116 | void add(Digest digest) { 117 | lastDigest = digest.bytes; 118 | for (var i = 0; i < digest.bytes.length; i++) { 119 | bytes.setUint8(i, bytes.getUint8(i) ^ lastDigest[i]); 120 | } 121 | } 122 | 123 | @override 124 | void close() {} 125 | } 126 | -------------------------------------------------------------------------------- /lib/salt.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | import 'dart:math'; 4 | 5 | /// Generates salts for hashing algorithms. 6 | class Salt { 7 | /// Generates a random salt of [length] bytes from a cryptographically secure random number generator. 8 | /// 9 | /// Each element of this list is a byte. 10 | static List generate(int length) { 11 | var buffer = new Uint8List(length); 12 | var rng = new Random.secure(); 13 | for (var i = 0; i < length; i++) { 14 | buffer[i] = rng.nextInt(256); 15 | } 16 | 17 | return buffer; 18 | } 19 | 20 | /// Generates a random salt of [length] bytes from a cryptographically secure random number generator and encodes it to Base64. 21 | /// 22 | /// [length] is the number of bytes generated, not the [length] of the base64 encoded string returned. Decoding 23 | /// the base64 encoded string will yield [length] number of bytes. 24 | static String generateAsBase64String(int length) { 25 | var encoder = new Base64Encoder(); 26 | return encoder.convert(generate(length)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: password_hash 2 | version: 2.0.0 3 | author: stable|kernel 4 | homepage: https://github.com/stablekernel/dart-password-hash 5 | description: PBKDF2 password hashing utility 6 | 7 | environment: 8 | sdk: ">=2.0.0 <3.0.0" 9 | 10 | dependencies: 11 | crypto: ^2.0.0 12 | 13 | dev_dependencies: 14 | test: ^1.3.0 15 | 16 | -------------------------------------------------------------------------------- /test/pbkdf2_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:password_hash/password_hash.dart'; 4 | import 'package:test/test.dart'; 5 | import 'package:crypto/crypto.dart'; 6 | 7 | List encodeBytes(String bytes) { 8 | var byteList = bytes.split(" "); 9 | var result = []; 10 | for (var byte in byteList) { 11 | result.add(int.parse(byte, radix: 16)); 12 | } 13 | return result; 14 | } 15 | 16 | void main() { 17 | group("Salt", () { 18 | test("Can generate random list of integers", () { 19 | var salt = Salt.generate(64); 20 | expect(salt.length, 64); 21 | 22 | expect(salt, everyElement(lessThan(256))); 23 | expect(salt, everyElement(greaterThanOrEqualTo(0))); 24 | }); 25 | 26 | test("Can generate base64 salt", () { 27 | var salt = Salt.generateAsBase64String(64); 28 | expect(salt is String, true); 29 | 30 | var decoded = new Base64Decoder().convert(salt); 31 | expect(decoded.length, 64); 32 | 33 | expect(decoded, everyElement(lessThan(256))); 34 | expect(decoded, everyElement(greaterThanOrEqualTo(0))); 35 | }); 36 | }); 37 | 38 | group("RFC6070", () { 39 | test("Disallow large values of derived key length", () { 40 | var sha = sha1; 41 | var hLen = sha.blockSize; 42 | var gen = new PBKDF2(hashAlgorithm: sha); 43 | 44 | try { 45 | gen.generateKey("password", "salt", 1, ((2 << 31) - 1) * hLen + 1); 46 | expect(true, false); 47 | } on PBKDF2Exception catch (e) { 48 | expect(e.toString(), contains("Derived key too long")); 49 | } 50 | }); 51 | 52 | test("Test vectors 1", () { 53 | var gen = new PBKDF2(hashAlgorithm: sha1); 54 | var output = gen.generateKey("password", "salt", 1, 20); 55 | expect( 56 | output, 57 | encodeBytes( 58 | "0c 60 c8 0f 96 1f 0e 71 f3 a9 b5 24 af 60 12 06 2f e0 37 a6")); 59 | }); 60 | 61 | test("Test vectors 2", () { 62 | var gen = new PBKDF2(hashAlgorithm: sha1); 63 | var output = gen.generateKey("password", "salt", 2, 20); 64 | expect( 65 | output, 66 | encodeBytes( 67 | "ea 6c 01 4d c7 2d 6f 8c cd 1e d9 2a ce 1d 41 f0 d8 de 89 57")); 68 | }); 69 | 70 | test("Test vectors 3", () { 71 | var gen = new PBKDF2(hashAlgorithm: sha1); 72 | var output = gen.generateKey("password", "salt", 4096, 20); 73 | expect( 74 | output, 75 | encodeBytes( 76 | "4b 00 79 01 b7 65 48 9a be ad 49 d9 26 f7 21 d0 65 a4 29 c1")); 77 | }); 78 | 79 | // This test may take a few minutes to run 80 | test("Test vectors 4", () { 81 | var gen = new PBKDF2(hashAlgorithm: sha1); 82 | var output = gen.generateKey("password", "salt", 16777216, 20); 83 | expect( 84 | output, 85 | encodeBytes( 86 | "ee fe 3d 61 cd 4d a4 e4 e9 94 5b 3d 6b a2 15 8c 26 34 e9 84")); 87 | }); 88 | 89 | test("Test vectors 5", () { 90 | var gen = new PBKDF2(hashAlgorithm: sha1); 91 | var output = gen.generateKey("passwordPASSWORDpassword", 92 | "saltSALTsaltSALTsaltSALTsaltSALTsalt", 4096, 25); 93 | expect( 94 | output, 95 | encodeBytes( 96 | "3d 2e ec 4f e4 1c 84 9b 80 c8 d8 36 62 c0 e4 4a 8b 29 1a 96 4c f2 f0 70 38")); 97 | }); 98 | 99 | test("Test vectors 6", () { 100 | var gen = new PBKDF2(hashAlgorithm: sha1); 101 | var output = gen.generateKey("pass\u0000word", "sa\u0000lt", 4096, 16); 102 | expect(output, 103 | encodeBytes("56 fa 6a a7 55 48 09 9d cc 37 d7 f0 34 25 e0 c3")); 104 | }); 105 | }); 106 | 107 | group("Sha256", () { 108 | test("Disallow large values of derived key length", () { 109 | var sha = sha256; 110 | var hLen = sha.blockSize; 111 | var gen = new PBKDF2(hashAlgorithm: sha); 112 | 113 | try { 114 | gen.generateKey("password", "salt", 1, ((2 << 31) - 1) * hLen + 1); 115 | expect(true, false); 116 | } on PBKDF2Exception catch (e) { 117 | expect(e.toString(), contains("Derived key too long")); 118 | } 119 | }); 120 | 121 | test("Test vectors 1", () { 122 | var gen = new PBKDF2(hashAlgorithm: sha256); 123 | var output = gen.generateKey("password", "salt", 1, 32); 124 | expect( 125 | output, 126 | encodeBytes( 127 | "12 0f b6 cf fc f8 b3 2c 43 e7 22 52 56 c4 f8 37 a8 65 48 c9 2c cc 35 48 08 05 98 7c b7 0b e1 7b")); 128 | }); 129 | 130 | test("Test vectors 2", () { 131 | var gen = new PBKDF2(hashAlgorithm: sha256); 132 | var output = gen.generateKey("password", "salt", 2, 32); 133 | expect( 134 | output, 135 | encodeBytes( 136 | "ae 4d 0c 95 af 6b 46 d3 2d 0a df f9 28 f0 6d d0 2a 30 3f 8e f3 c2 51 df d6 e2 d8 5a 95 47 4c 43")); 137 | }); 138 | 139 | test("Test vectors 3", () { 140 | var gen = new PBKDF2(hashAlgorithm: sha256); 141 | var output = gen.generateKey("password", "salt", 4096, 32); 142 | expect( 143 | output, 144 | encodeBytes( 145 | "c5 e4 78 d5 92 88 c8 41 aa 53 0d b6 84 5c 4c 8d 96 28 93 a0 01 ce 4e 11 a4 96 38 73 aa 98 13 4a")); 146 | }); 147 | 148 | // This test may take a few minutes to run 149 | test("Test vectors 4", () { 150 | var gen = new PBKDF2(hashAlgorithm: sha256); 151 | var output = gen.generateKey("password", "salt", 16777216, 32); 152 | expect( 153 | output, 154 | encodeBytes( 155 | "cf 81 c6 6f e8 cf c0 4d 1f 31 ec b6 5d ab 40 89 f7 f1 79 e8 9b 3b 0b cb 17 ad 10 e3 ac 6e ba 46")); 156 | }); 157 | 158 | test("Test vectors 5", () { 159 | var gen = new PBKDF2(hashAlgorithm: sha256); 160 | var output = gen.generateKey("passwordPASSWORDpassword", 161 | "saltSALTsaltSALTsaltSALTsaltSALTsalt", 4096, 40); 162 | expect( 163 | output, 164 | encodeBytes( 165 | "34 8c 89 db cb d3 2b 2f 32 d8 14 b8 11 6e 84 cf 2b 17 34 7e bc 18 00 18 1c 4e 2a 1f b8 dd 53 e1 c6 35 51 8c 7d ac 47 e9")); 166 | }); 167 | 168 | test("Test vectors 6", () { 169 | var gen = new PBKDF2(hashAlgorithm: sha256); 170 | var output = gen.generateKey("pass\u0000word", "sa\u0000lt", 4096, 16); 171 | expect(output, 172 | encodeBytes("89 b6 9d 05 16 f8 29 89 3c 69 62 26 65 0a 86 87")); 173 | }); 174 | }); 175 | } 176 | --------------------------------------------------------------------------------