salsa20 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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