salsa20 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,87 @@
1
+ #include <ruby.h>
2
+ #include "ecrypt-sync.h"
3
+
4
+ /* Older versions of Ruby (< 1.8.6) need these */
5
+ #ifndef RSTRING_PTR
6
+ #define RSTRING_PTR(s) (RSTRING(s)->ptr)
7
+ #endif
8
+ #ifndef RSTRING_LEN
9
+ #define RSTRING_LEN(s) (RSTRING(s)->len)
10
+ #endif
11
+ #ifndef RARRAY_PTR
12
+ #define RARRAY_PTR(s) (RARRAY(s)->ptr)
13
+ #endif
14
+ #ifndef RARRAY_LEN
15
+ #define RARRAY_LEN(s) (RARRAY(s)->len)
16
+ #endif
17
+
18
+ static VALUE cSalsa20;
19
+
20
+ static VALUE rb_salsa20_alloc(VALUE klass) {
21
+ VALUE obj;
22
+ ECRYPT_ctx *ctx;
23
+
24
+ obj = Data_Make_Struct(klass, ECRYPT_ctx, 0, 0, ctx);
25
+ return obj;
26
+ }
27
+
28
+ static VALUE rb_salsa20_init_context(VALUE self) {
29
+ VALUE key, iv;
30
+ ECRYPT_ctx *ctx;
31
+
32
+ Data_Get_Struct(self, ECRYPT_ctx, ctx);
33
+ key = rb_iv_get(self, "@key");
34
+ iv = rb_iv_get(self, "@iv");
35
+
36
+ ECRYPT_keysetup(ctx, (const unsigned char*)RSTRING_PTR(key), (unsigned int)RSTRING_LEN(key) * 8, 64);
37
+ ECRYPT_ivsetup(ctx, (const unsigned char*)RSTRING_PTR(iv));
38
+
39
+ return self;
40
+ }
41
+
42
+ static VALUE rb_salsa20_encrypt_or_decrypt(int argc, VALUE * argv, VALUE self) {
43
+ VALUE input, output;
44
+ ECRYPT_ctx *ctx;
45
+
46
+ Data_Get_Struct(self, ECRYPT_ctx, ctx);
47
+
48
+ rb_scan_args(argc, argv, "1", &input);
49
+ Check_Type(input, T_STRING);
50
+
51
+ output = rb_str_new(0, RSTRING_LEN(input));
52
+ ECRYPT_encrypt_bytes(ctx, (const unsigned char*)RSTRING_PTR(input), (unsigned char*)RSTRING_PTR(output), (unsigned int)RSTRING_LEN(input));
53
+
54
+ return output;
55
+ }
56
+
57
+ static VALUE rb_salsa20_set_cipher_position(int argc, VALUE * argv, VALUE self) {
58
+ VALUE low_32bits, high_32bits;
59
+ ECRYPT_ctx *ctx;
60
+
61
+ Data_Get_Struct(self, ECRYPT_ctx, ctx);
62
+
63
+ rb_scan_args(argc, argv, "2", &low_32bits, &high_32bits);
64
+ ctx->input[8] = NUM2INT(low_32bits);
65
+ ctx->input[9] = NUM2INT(high_32bits);
66
+
67
+ return Qnil;
68
+ }
69
+
70
+ static VALUE rb_salsa20_get_cipher_position(VALUE self) {
71
+ ECRYPT_ctx *ctx;
72
+
73
+ Data_Get_Struct(self, ECRYPT_ctx, ctx);
74
+
75
+ return rb_ull2inum(((unsigned LONG_LONG)(ctx->input[9]) << 32) | (unsigned LONG_LONG)(ctx->input[8]));
76
+ }
77
+
78
+ void Init_salsa20_ext() {
79
+ cSalsa20 = rb_define_class("Salsa20", rb_cObject);
80
+
81
+ rb_define_alloc_func(cSalsa20, rb_salsa20_alloc);
82
+
83
+ rb_define_private_method(cSalsa20, "init_context", rb_salsa20_init_context, 0);
84
+ rb_define_private_method(cSalsa20, "encrypt_or_decrypt", rb_salsa20_encrypt_or_decrypt, -1);
85
+ rb_define_private_method(cSalsa20, "set_cipher_position", rb_salsa20_set_cipher_position, -1);
86
+ rb_define_private_method(cSalsa20, "get_cipher_position", rb_salsa20_get_cipher_position, 0);
87
+ }
@@ -0,0 +1,105 @@
1
+ require 'salsa20_ext'
2
+
3
+ # Salsa20 stream cipher engine. Initialize the engine with +key+ and +iv+, and
4
+ # then call Salsa20#encrypt or Salsa20#decrypt (they are actually identical --
5
+ # that's how stream ciphers work).
6
+ #
7
+ # Example:
8
+ #
9
+ # encryptor = Salsa20.new(key_str, iv_str)
10
+ # cipher_text = encryptor.encrypt(plain_text)
11
+ #
12
+ class Salsa20
13
+
14
+ # Salsa20 engine was initialized withe a key of the wrong length (see Salsa20#new).
15
+ class InvalidKeyError < StandardError
16
+ end
17
+
18
+ # Salsa20#encrypt was called after a non 64-bytes boundry block
19
+ class EngineClosedError < StandardError
20
+ end
21
+
22
+ # Salsa20#seek was called with a non 64-bytes boundry position
23
+ class IllegalSeekError < StandardError
24
+ end
25
+
26
+ # The encryption key
27
+ attr_reader :key
28
+
29
+ # The encryption IV (Initialization Vector) / nonce
30
+ attr_reader :iv
31
+
32
+ # Create a new Salsa20 encryption/decryption engine.
33
+ #
34
+ # +key+ is the encryption key and must be exactly 128-bits (16 bytes) or
35
+ # 256-bits (32 bytes) long
36
+ #
37
+ # +iv+ is the encryption IV and must be exactly 64-bits (8 bytes) long
38
+ #
39
+ # If +key+ or +iv+ lengths are invalid then a Salsa20::InvalidKeyError
40
+ # exception is raised.
41
+ def initialize(key, iv)
42
+ # do all the possible checks here to make sure the C extension code gets clean variables
43
+ raise TypeError, "key must be a String" unless key.is_a? String
44
+ raise TypeError, "iv must be a String" unless iv.is_a? String
45
+
46
+ raise InvalidKeyError, "key length must be 16 or 32 bytes" unless key.size == 16 || key.size == 32
47
+ raise InvalidKeyError, "iv length must be 8 bytes" unless iv.size == 8
48
+
49
+ @key = key
50
+ @iv = iv
51
+ @closed = false
52
+ init_context # Implemented in the C extension
53
+ end
54
+
55
+ # Returns _true_ if the last encryption was of a non 64-bytes boundry chunk.
56
+ # This means this instance cannot be further used (subsequent calls to
57
+ # Salsa20#encrypt or Salsa20#decrypt will raise a Salsa20::EngineClosedError
58
+ # exception); _false_ if the instance can be further used to encrypt/decrypt
59
+ # additional chunks.
60
+ def closed?
61
+ @closed
62
+ end
63
+
64
+ # Encrypts/decrypts the string +input+. If +input+ length is on 64-bytes
65
+ # boundry, you may call encrypt (or decrypt) again; once you call it with a
66
+ # non 64-bytes boundry chunk this must be the final chunk (subsequent calls will
67
+ # raise a Salsa20::EngineClosedError exception).
68
+ #
69
+ # Returns the encrypted/decrypted string, which has the same size as the
70
+ # input string.
71
+ def encrypt(input)
72
+ raise TypeError, "input must be a string" unless input.is_a? String
73
+ raise EngineClosedError, "instance is closed" if closed?
74
+ @closed = true if (input.size % 64) != 0
75
+ encrypt_or_decrypt(input) # Implemented in the C extension
76
+ end
77
+
78
+ alias :decrypt :encrypt
79
+
80
+ # Advance the cipher engine into +position+ (given in bytes). This can be
81
+ # used to start decrypting from the middle of a file, for example.
82
+ #
83
+ # Note: +position+ must be on a 64-bytes boundry (otherwise a
84
+ # Salsa20::IllegalSeekError exception is raised).
85
+ def seek(position)
86
+ raise IllegalSeekError, "seek position must be on 64-bytes boundry" unless position % 64 == 0
87
+ position /= 64
88
+ set_cipher_position(low_32bits(position), high_32bits(position)) # Implemented in the C extension
89
+ end
90
+
91
+ # Returns the current cipher stream position in bytes
92
+ def position
93
+ get_cipher_position * 64
94
+ end
95
+
96
+ private
97
+
98
+ def low_32bits(n)
99
+ n & 0xffffffff
100
+ end
101
+
102
+ def high_32bits(n)
103
+ (n >> 32) & 0xffffffff
104
+ end
105
+ end
@@ -0,0 +1,28 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'salsa20'
3
+ s.version = '0.1.0'
4
+
5
+ s.summary = "Salsa20 stream cipher algorithm."
6
+ s.description = <<-EOF
7
+ Salsa20 is a stream cipher algorithm designed by Daniel Bernstein. salsa20-ruby provides
8
+ a simple Ruby wrapper.
9
+ EOF
10
+
11
+ s.files = `git ls-files`.split("\n")
12
+ s.require_path = 'lib'
13
+
14
+ s.test_files = `git ls-files test`.split("\n")
15
+
16
+ s.add_development_dependency 'rdoc'
17
+ s.add_development_dependency 'rake-compiler'
18
+
19
+ s.has_rdoc = true
20
+ s.rdoc_options += ['--title', 'salsa20', '--main', 'README.rdoc']
21
+ s.extra_rdoc_files += ['README.rdoc', 'LICENSE', 'CHANGELOG', 'lib/salsa20.rb']
22
+
23
+ s.extensions = 'ext/salsa20_ext/extconf.rb'
24
+
25
+ s.authors = ["Dov Murik"]
26
+ s.email = "dov.murik@gmail.com"
27
+ s.homepage = "https://github.com/dubek/salsa20-ruby"
28
+ end
@@ -0,0 +1,140 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), "..", "lib", "salsa20"))
2
+ require 'test/unit'
3
+
4
+ class Salsa20Test < Test::Unit::TestCase
5
+ def test_salsa20_keystream
6
+ expected = "@\x8D\x94\xF48f9Z)\e\xBD\xB8?\xCC\xEC\xD6g\xB3;\xC7ev\v\xCA]\xEE\x19I;\xA2<\\^\xCEFQn\x94B{+\x06\xE2\x85\x9F\xEC\xBBp@\xA4\x8F\xD8~\xD3\x12\x197\f\xD7'\x8C\xC8\xEF\xFC"
7
+ assert_equal expected, Salsa20.new("K"*32, "I"*8).encrypt("\x00"*64)
8
+ end
9
+
10
+ def test_bad_number_of_arguments_for_new_should_raise_exception
11
+ assert_raise(ArgumentError) { Salsa20.new }
12
+ assert_raise(ArgumentError) { Salsa20.new("K"*32) }
13
+ assert_raise(ArgumentError) { Salsa20.new("K"*32, "I"*8, "third") }
14
+ end
15
+
16
+ def test_non_string_arguments_for_new_should_raise_exception
17
+ assert_raise(TypeError) { Salsa20.new([1,2,3], "I"*8) }
18
+ assert_raise(TypeError) { Salsa20.new("K"*32, { "a" => "b"}) }
19
+ assert_raise(TypeError) { Salsa20.new([1,2,3], { "a" => "b"}) }
20
+ end
21
+
22
+ def test_invalid_key_length_should_raise_exception
23
+ assert_raise(Salsa20::InvalidKeyError) { Salsa20.new("K"*15, "I"*8) }
24
+ assert_nothing_raised { Salsa20.new("K"*16, "I"*8) }
25
+ assert_raise(Salsa20::InvalidKeyError) { Salsa20.new("K"*17, "I"*8) }
26
+ assert_raise(Salsa20::InvalidKeyError) { Salsa20.new("K"*31, "I"*8) }
27
+ assert_nothing_raised { Salsa20.new("K"*32, "I"*8) }
28
+ assert_raise(Salsa20::InvalidKeyError) { Salsa20.new("K"*33, "I"*8) }
29
+ end
30
+
31
+ def test_invalid_iv_length_should_raise_exception
32
+ assert_raise(Salsa20::InvalidKeyError) { Salsa20.new("K"*32, "I"*7) }
33
+ assert_nothing_raised { Salsa20.new("K"*32, "I"*8) }
34
+ assert_raise(Salsa20::InvalidKeyError) { Salsa20.new("K"*32, "I"*9) }
35
+ assert_raise(Salsa20::InvalidKeyError) { Salsa20.new("K"*32, "I"*16) }
36
+ end
37
+
38
+ def test_accessors
39
+ the_key = "K"*32
40
+ the_iv = "I"*8
41
+ encryptor = Salsa20.new(the_key, the_iv)
42
+ assert_equal the_key, encryptor.key
43
+ assert_equal the_iv, encryptor.iv
44
+ end
45
+
46
+ def test_encrypt_and_decrypt_with_256_bit_key
47
+ the_key = "A"*32
48
+ the_iv = "B"*8
49
+ plain_text = "the quick brown fox jumped over the lazy dog"
50
+ encryptor = Salsa20.new(the_key, the_iv)
51
+ cipher_text = encryptor.encrypt(plain_text)
52
+ assert_equal plain_text.size, cipher_text.size
53
+ decryptor = Salsa20.new(the_key, the_iv)
54
+ assert_equal plain_text, decryptor.decrypt(cipher_text)
55
+ end
56
+
57
+ def test_encrypted_encoding_should_be_binary
58
+ return unless "TEST".respond_to?(:encoding)
59
+ save_encoding = Encoding.default_external
60
+ Encoding.default_external = "UTF-8"
61
+ the_key = "A"*32
62
+ the_iv = "B"*8
63
+ plain_text = "the quick brown fox jumped over the lazy dog"
64
+ encryptor = Salsa20.new(the_key, the_iv)
65
+ cipher_text = encryptor.encrypt(plain_text)
66
+ assert_equal Encoding.find("ASCII-8BIT"), cipher_text.encoding
67
+ assert_equal Encoding.find("BINARY"), cipher_text.encoding
68
+ Encoding.default_external = save_encoding
69
+ end
70
+
71
+ def test_encrypt_and_decrypt_with_128_bit_key
72
+ the_key = "C"*16
73
+ the_iv = "D"*8
74
+ plain_text = "the quick brown fox jumped over the lazy dog"
75
+ encryptor = Salsa20.new(the_key, the_iv)
76
+ cipher_text = encryptor.encrypt(plain_text)
77
+ assert_equal plain_text.size, cipher_text.size
78
+ decryptor = Salsa20.new(the_key, the_iv)
79
+ assert_equal plain_text, decryptor.decrypt(cipher_text)
80
+ end
81
+
82
+ def test_multiple_encrypt_and_one_decrypt
83
+ the_key = "E"*32
84
+ the_iv = "F"*8
85
+ plain_text = "the quick brown fox jumped over the lazy dog" * 5
86
+ parts = [ plain_text[0,64], plain_text[64,64], plain_text[128,64], plain_text[192,64] ]
87
+ assert_equal plain_text, parts.join
88
+ encryptor = Salsa20.new(the_key, the_iv)
89
+ cipher_text = parts.map { |part| encryptor.encrypt(part) }.join
90
+ assert_equal true, encryptor.closed?
91
+ assert_equal plain_text.size, cipher_text.size
92
+ decryptor = Salsa20.new(the_key, the_iv)
93
+ assert_equal plain_text, decryptor.decrypt(cipher_text)
94
+ assert_equal true, decryptor.closed?
95
+ end
96
+
97
+ def test_encrypt_after_non_64_bytes_should_raise_exception
98
+ the_key = "G"*32
99
+ the_iv = "H"*8
100
+ part1 = "a"*63
101
+ part2 = "b"*64
102
+ encryptor = Salsa20.new(the_key, the_iv)
103
+ assert_equal false, encryptor.closed?
104
+ encryptor.encrypt(part1)
105
+ assert_equal true, encryptor.closed?
106
+ assert_raise(Salsa20::EngineClosedError) { encryptor.encrypt(part2) }
107
+ end
108
+
109
+ def test_seek
110
+ the_key = "I"*32
111
+ the_iv = "J"*8
112
+ plain_text = "the quick brown fox jumped over the lazy dog" * 5
113
+ encryptor = Salsa20.new(the_key, the_iv)
114
+ cipher_text = encryptor.encrypt(plain_text)
115
+ decryptor = Salsa20.new(the_key, the_iv)
116
+ assert_equal 0, decryptor.position
117
+ cipher_text.slice!(0,128)
118
+ decryptor.seek(128)
119
+ assert_equal 128, decryptor.position
120
+ assert_equal plain_text[128..-1], decryptor.decrypt(cipher_text)
121
+ assert_equal 256, decryptor.position
122
+ end
123
+
124
+ def test_seek_to_non_64_bytes_boundry_should_raise_exception
125
+ the_key = "K"*32
126
+ the_iv = "L"*8
127
+ encryptor = Salsa20.new(the_key, the_iv)
128
+ assert_raise(Salsa20::IllegalSeekError) { encryptor.seek(65) }
129
+ end
130
+
131
+ def test_seek_and_position_to_large_positions
132
+ the_key = "M"*32
133
+ the_iv = "N"*8
134
+ large_position = 1 << 50
135
+ encryptor = Salsa20.new(the_key, the_iv)
136
+ assert_equal 0, encryptor.position
137
+ encryptor.seek(large_position)
138
+ assert_equal large_position, encryptor.position
139
+ end
140
+ end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: salsa20
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Dov Murik
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-08-20 00:00:00.000000000 +03:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rdoc
17
+ requirement: &2156666300 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :development
24
+ prerelease: false
25
+ version_requirements: *2156666300
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake-compiler
28
+ requirement: &2156665860 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: *2156665860
37
+ description: ! " Salsa20 is a stream cipher algorithm designed by Daniel Bernstein.
38
+ salsa20-ruby provides\n a simple Ruby wrapper.\n"
39
+ email: dov.murik@gmail.com
40
+ executables: []
41
+ extensions:
42
+ - ext/salsa20_ext/extconf.rb
43
+ extra_rdoc_files:
44
+ - README.rdoc
45
+ - LICENSE
46
+ - CHANGELOG
47
+ - lib/salsa20.rb
48
+ files:
49
+ - .gitignore
50
+ - CHANGELOG
51
+ - LICENSE
52
+ - README.rdoc
53
+ - Rakefile
54
+ - ext/salsa20_ext/ecrypt-config.h
55
+ - ext/salsa20_ext/ecrypt-machine.h
56
+ - ext/salsa20_ext/ecrypt-portable.h
57
+ - ext/salsa20_ext/ecrypt-sync.h
58
+ - ext/salsa20_ext/extconf.rb
59
+ - ext/salsa20_ext/salsa20.c
60
+ - ext/salsa20_ext/salsa20_ext.c
61
+ - lib/salsa20.rb
62
+ - salsa20.gemspec
63
+ - test/salsa20_test.rb
64
+ has_rdoc: true
65
+ homepage: https://github.com/dubek/salsa20-ruby
66
+ licenses: []
67
+ post_install_message:
68
+ rdoc_options:
69
+ - --title
70
+ - salsa20
71
+ - --main
72
+ - README.rdoc
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ! '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ! '>='
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubyforge_project:
89
+ rubygems_version: 1.6.2
90
+ signing_key:
91
+ specification_version: 3
92
+ summary: Salsa20 stream cipher algorithm.
93
+ test_files:
94
+ - test/salsa20_test.rb