sodium 0.0.0 → 0.5.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.
Files changed (47) hide show
  1. data/.gitignore +4 -0
  2. data/.travis.yml +27 -0
  3. data/Gemfile +9 -0
  4. data/README.md +111 -0
  5. data/Rakefile +3 -0
  6. data/VERSION +1 -0
  7. data/config/nacl_ffi.yml +90 -0
  8. data/lib/sodium.rb +24 -0
  9. data/lib/sodium/auth.rb +52 -0
  10. data/lib/sodium/box.rb +127 -0
  11. data/lib/sodium/buffer.rb +141 -0
  12. data/lib/sodium/delegate.rb +58 -0
  13. data/lib/sodium/ffi.rb +9 -0
  14. data/lib/sodium/ffi/crypto.rb +106 -0
  15. data/lib/sodium/ffi/lib_c.rb +9 -0
  16. data/lib/sodium/ffi/random.rb +11 -0
  17. data/lib/sodium/hash.rb +23 -0
  18. data/lib/sodium/one_time_auth.rb +52 -0
  19. data/lib/sodium/random.rb +16 -0
  20. data/lib/sodium/secret_box.rb +65 -0
  21. data/lib/sodium/sign.rb +75 -0
  22. data/lib/sodium/version.rb +5 -0
  23. data/sodium.gemspec +9 -3
  24. data/sodium.pub.gpg +37 -0
  25. data/tasks/libsodium.rake +70 -0
  26. data/tasks/test.rake +6 -0
  27. data/tasks/version.rake +3 -0
  28. data/test/sodium/auth/hmacsha256_test.rb +54 -0
  29. data/test/sodium/auth/hmacsha512256_test.rb +53 -0
  30. data/test/sodium/auth_test.rb +49 -0
  31. data/test/sodium/box/curve25519xsalsa20poly1305_test.rb +79 -0
  32. data/test/sodium/box_test.rb +109 -0
  33. data/test/sodium/buffer_test.rb +120 -0
  34. data/test/sodium/delegate_test.rb +44 -0
  35. data/test/sodium/hash/sha256_test.rb +31 -0
  36. data/test/sodium/hash/sha512_test.rb +35 -0
  37. data/test/sodium/hash_test.rb +35 -0
  38. data/test/sodium/one_time_auth/poly1305_test.rb +54 -0
  39. data/test/sodium/one_time_auth_test.rb +49 -0
  40. data/test/sodium/random_test.rb +25 -0
  41. data/test/sodium/secret_box/xsalsa20poly1305_test.rb +50 -0
  42. data/test/sodium/secret_box_test.rb +56 -0
  43. data/test/sodium/sign/ed25519_test.rb +52 -0
  44. data/test/sodium/sign_test.rb +58 -0
  45. data/test/test_helper.rb +44 -0
  46. metadata +117 -8
  47. checksums.yaml +0 -7
@@ -0,0 +1,141 @@
1
+ require 'sodium'
2
+ require 'securerandom'
3
+
4
+ class Sodium::Buffer
5
+ def self.key(size)
6
+ Sodium::Random.bytes(size)
7
+ end
8
+
9
+ def self.nonce(size)
10
+ Sodium::Random.bytes(size)
11
+ end
12
+
13
+ def self.empty(size)
14
+ self.new("\0" * size).tap {|buffer| yield buffer if block_given? }
15
+ end
16
+
17
+ def self.new(bytes, size = bytes.bytesize)
18
+ raise Sodium::LengthError, "buffer must be exactly #{size} bytes long" unless
19
+ bytes.bytesize == size
20
+
21
+ bytes.kind_of?(self) ?
22
+ bytes :
23
+ super(bytes)
24
+ end
25
+
26
+ def initialize(bytes)
27
+ # initialize with a forced hard copy of the bytes (Ruby is smart
28
+ # about using copy-on-write for strings 24 bytes or longer, so we
29
+ # have to perform a no-op that forces Ruby to copy the bytes)
30
+ @bytes = bytes.tr('','').tap {|s|
31
+ s.force_encoding('BINARY') if
32
+ s.respond_to?(:force_encoding)
33
+ }.freeze
34
+
35
+ self.class._mlock! @bytes
36
+ self.class._mwipe! bytes
37
+
38
+ ObjectSpace.define_finalizer self,
39
+ self.class._finalizer(@bytes)
40
+ end
41
+
42
+ def +(other)
43
+ Sodium::Buffer.new(
44
+ self.to_str +
45
+ Sodium::Buffer.new(other).to_str
46
+ )
47
+ end
48
+
49
+ def pad(size)
50
+ self.class.new(
51
+ ("\0" * size) + @bytes
52
+ )
53
+ end
54
+
55
+ def unpad(size)
56
+ self.byteslice(size .. -1)
57
+ end
58
+
59
+ def byteslice(*args)
60
+ return self.class.new(
61
+ @bytes.byteslice(*args).to_s
62
+ ) if (
63
+ # Ruby 1.8 doesn't have byteslice
64
+ @bytes.respond_to?(:byteslice) or
65
+
66
+ # JRuby reuses memory regions when calling byteslice, which
67
+ # results in them getting cleared when the new buffer initializes
68
+ defined?(RUBY_ENGINE) and RUBY_ENGINE == 'java'
69
+ )
70
+
71
+ raise ArgumentError, 'wrong number of arguments (0 for 1..2)' if
72
+ args.length < 1 or args.length > 2
73
+
74
+ start, finish = case
75
+ when args[1]
76
+ # matches: byteslice(start, size)
77
+ start = args[0].to_i
78
+ size = args[1].to_i
79
+
80
+ # if size is less than 1, finish needs to be small enough that
81
+ # `finish - start + 1 <= 0` even after finish is wrapped
82
+ # around to account for negative indices
83
+ finish = size > 0 ?
84
+ start + size - 1 :
85
+ - self.bytesize.succ
86
+
87
+ [ start, finish ]
88
+ when args[0].kind_of?(Range)
89
+ # matches: byteslice(start .. finish)
90
+ # matches: byteslice(start ... finish)
91
+ range = args[0]
92
+ start = range.begin.to_i
93
+ finish = range.exclude_end? ? range.end.to_i - 1 : range.end.to_i
94
+
95
+ [ start, finish ]
96
+ else
97
+ # matches: byteslice(start)
98
+ [ args[0].to_i, args[0].to_i ]
99
+ end
100
+
101
+ # ensure negative values are wrapped around explicitly
102
+ start += self.bytesize if start < 0
103
+ finish += self.bytesize if finish < 0
104
+ size = finish - start + 1
105
+
106
+ bytes = (start >= 0 and size >= 0) ?
107
+ @bytes.unpack("@#{start}a#{size}").first :
108
+ ''
109
+
110
+ self.class.new(bytes)
111
+ end
112
+
113
+ def bytesize
114
+ @bytes.bytesize
115
+ end
116
+
117
+ def inspect
118
+ # this appears to be equivalent to the default Object#inspect,
119
+ # albeit without instance variables
120
+ "#<%s:0x%x>" % [ self.class.name, self.__id__ * 2 ]
121
+ end
122
+
123
+ def to_str
124
+ @bytes.to_str
125
+ end
126
+
127
+ private
128
+
129
+ def self._finalizer(buffer)
130
+ proc { self._mwipe!(buffer) }
131
+ end
132
+
133
+ def self._mwipe!(buffer)
134
+ Sodium::FFI::Crypto.sodium_memzero(buffer, buffer.bytesize)
135
+ end
136
+
137
+ def self._mlock!(buffer)
138
+ Sodium::FFI::LibC.mlock(buffer, buffer.bytesize) or
139
+ raise Sodium::MemoryError, 'could not mlock(2) secure buffer into memory'
140
+ end
141
+ end
@@ -0,0 +1,58 @@
1
+ require 'sodium'
2
+
3
+ module Sodium::Delegate
4
+ def self.included(base)
5
+ base.send :extend, ClassMethods
6
+ base.send :extend, self.class_methods(base)
7
+ end
8
+
9
+ module ClassMethods
10
+ def inherited(base)
11
+ @_nacl_implementations ||= []
12
+ @_nacl_implementations << base
13
+
14
+ class << base
15
+ undef_method :implementation=
16
+ define_method(:implementation) { self }
17
+ end
18
+ end
19
+
20
+ def primitive
21
+ self.implementation[:PRIMITIVE].to_sym
22
+ end
23
+
24
+ def implementation(name = nil)
25
+ name ? _find_implementation(name) :
26
+ @_nacl_default ||= _find_implementation(self::DEFAULT)
27
+ end
28
+
29
+ def implementation=(implementation)
30
+ @_nacl_default = implementation
31
+ end
32
+
33
+ private
34
+
35
+ def _find_implementation(name)
36
+ @_nacl_implementations.detect {|i| i.primitive == name }
37
+ end
38
+ end
39
+
40
+ def self.class_methods(base)
41
+ Module.new do
42
+ define_method :new do |*args, &block|
43
+ return super(*args, &block) unless self == base
44
+ return self.implementation.new(*args, &block)
45
+ end
46
+ end
47
+ end
48
+
49
+ def primitive
50
+ self.class.primitive
51
+ end
52
+
53
+ protected
54
+
55
+ def implementation
56
+ self.class.implementation
57
+ end
58
+ end
data/lib/sodium/ffi.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'sodium'
2
+ require 'ffi'
3
+
4
+ module Sodium::FFI
5
+ end
6
+
7
+ require 'sodium/ffi/lib_c'
8
+ require 'sodium/ffi/crypto'
9
+ require 'sodium/ffi/random'
@@ -0,0 +1,106 @@
1
+ require 'sodium/ffi'
2
+ require 'yaml'
3
+
4
+ module Sodium::FFI::Crypto
5
+ CONFIG_PATH = File.expand_path('../../../../config/nacl_ffi.yml', __FILE__)
6
+ CONFIG = YAML.load_file(CONFIG_PATH)
7
+
8
+ def self._install_default(delegate, configuration)
9
+ family = configuration[:family]
10
+ method = _install_function delegate, family, nil, :PRIMITIVE, [ :string ]
11
+ primitive = delegate.send(method)
12
+ rescue FFI::NotFoundError
13
+ primitive = configuration[:primitives].first
14
+ ensure
15
+ delegate.const_set :DEFAULT, primitive.to_s.downcase.to_sym
16
+ end
17
+
18
+ def self._install_primitives(delegate, configuration)
19
+ configuration[:primitives].each do |primitive|
20
+ subclass = Class.new(delegate) do
21
+ def self.[](name)
22
+ self.const_get(name)
23
+ end
24
+ end
25
+
26
+ delegate.const_set primitive, subclass
27
+
28
+ _install_constants subclass, configuration[:family], primitive,
29
+ configuration[:constants]
30
+
31
+ _install_functions subclass, configuration[:family], primitive,
32
+ configuration[:functions]
33
+ end
34
+ end
35
+
36
+ def self._install_constants(subclass, family, primitive, constants)
37
+ constants = constants.inject(
38
+ :PRIMITIVE => :string
39
+ ) {|hash, constant| hash.update(constant => :size_t) }
40
+
41
+ constants.each_pair do |name, type|
42
+ _install_constant(subclass, family, primitive, name, type)
43
+ end
44
+ end
45
+
46
+ def self._install_constant(subclass, family, primitive, name, type)
47
+ method = _install_function subclass, family, primitive, name, [ type ]
48
+
49
+ family = family.to_s.upcase
50
+ name = name.to_s.upcase
51
+ value = subclass.send(method)
52
+
53
+ self. const_set("#{family}_#{primitive}_#{name}", value)
54
+ subclass.const_set(name, value)
55
+ end
56
+
57
+ def self._install_functions(subclass, family, primitive, methods)
58
+ methods.each do |name, arguments|
59
+ _install_function(subclass, family, primitive, name, arguments, &:zero?)
60
+ end
61
+ end
62
+
63
+ def self._install_function(subclass, family, primitive, name, arguments, &converter)
64
+ imp = [ family, primitive, name ].compact.join('_').downcase
65
+ meth = [ 'nacl', name ].compact.join('_').downcase
66
+ arguments = arguments.map(&:to_sym)
67
+
68
+ self.attach_function imp, arguments[0..-2], arguments.last
69
+ self.attach_method imp, self, subclass, meth, &converter
70
+
71
+ meth
72
+ end
73
+
74
+ def self.attach_method(implementation, nacl, delegate, method)
75
+ self._metaclass(delegate).send :define_method, method do |*a, &b|
76
+ value = nacl.send(implementation, *a, &b)
77
+ block_given? ? yield(value) : value
78
+ end
79
+ end
80
+
81
+ def self._load_class(name)
82
+ name.split('::').inject(Object) {|klass, part| klass.const_get(part) }
83
+ end
84
+
85
+ def self._metaclass(klass)
86
+ (class << klass; self; end)
87
+ end
88
+ end
89
+
90
+ module Sodium::FFI::Crypto
91
+ extend FFI::Library
92
+
93
+ ffi_lib 'sodium'
94
+
95
+ attach_function 'sodium_init', [], :void
96
+ attach_function 'sodium_memzero', [:pointer, :size_t], :void
97
+
98
+ sodium_init
99
+
100
+ CONFIG.each do |configuration|
101
+ delegate = self._load_class configuration[:class]
102
+
103
+ self._install_default delegate, configuration
104
+ self._install_primitives delegate, configuration
105
+ end
106
+ end
@@ -0,0 +1,9 @@
1
+ require 'sodium/ffi'
2
+
3
+ module Sodium::FFI::LibC
4
+ extend FFI::Library
5
+
6
+ ffi_lib FFI::Library::LIBC
7
+
8
+ attach_function 'mlock', [:pointer, :size_t], :int
9
+ end
@@ -0,0 +1,11 @@
1
+ require 'sodium/ffi'
2
+
3
+ module Sodium::FFI::Random
4
+ extend FFI::Library
5
+
6
+ ffi_lib 'sodium'
7
+
8
+ attach_function 'randombytes_random', [], :uint32
9
+ attach_function 'randombytes_uniform', [:uint32], :uint32
10
+ attach_function 'randombytes_buf', [:pointer, :size_t], :void
11
+ end
@@ -0,0 +1,23 @@
1
+ require 'sodium'
2
+
3
+ class Sodium::Hash
4
+ include Sodium::Delegate
5
+
6
+ def self.hash(message)
7
+ message = _message(message)
8
+
9
+ Sodium::Buffer.empty self.implementation[:BYTES] do |digest|
10
+ self.implementation.nacl(
11
+ digest.to_str,
12
+ message.to_str,
13
+ message.to_str.bytesize
14
+ ) or raise Sodium::CryptoError, 'failed to generate a hash for the message'
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def self._message(m)
21
+ Sodium::Buffer.new m
22
+ end
23
+ end
@@ -0,0 +1,52 @@
1
+ require 'sodium'
2
+
3
+ class Sodium::OneTimeAuth
4
+ include Sodium::Delegate
5
+
6
+ def self.key
7
+ Sodium::Buffer.key self.implementation[:KEYBYTES]
8
+ end
9
+
10
+ def initialize(key)
11
+ @key = self.class._key(key)
12
+ end
13
+
14
+ def one_time_auth(message)
15
+ message = self.class._message(message)
16
+
17
+ Sodium::Buffer.empty self.implementation[:BYTES] do |authenticator|
18
+ self.implementation.nacl(
19
+ authenticator.to_str,
20
+ message.to_str,
21
+ message.to_str.bytesize,
22
+ @key.to_str
23
+ ) or raise Sodium::CryptoError, 'failed to generate an authenticator'
24
+ end
25
+ end
26
+
27
+ def verify(message, authenticator)
28
+ message = self.class._message(message)
29
+ authenticator = self.class._authenticator(authenticator)
30
+
31
+ self.implementation.nacl_verify(
32
+ authenticator.to_str,
33
+ message.to_str,
34
+ message.to_str.bytesize,
35
+ @key.to_str
36
+ )
37
+ end
38
+
39
+ private
40
+
41
+ def self._key(k)
42
+ Sodium::Buffer.new k, self.implementation[:KEYBYTES]
43
+ end
44
+
45
+ def self._authenticator(a)
46
+ Sodium::Buffer.new a, self.implementation[:BYTES]
47
+ end
48
+
49
+ def self._message(m)
50
+ Sodium::Buffer.new m
51
+ end
52
+ end
@@ -0,0 +1,16 @@
1
+ require 'sodium'
2
+
3
+ module Sodium::Random
4
+ def self.bytes(size)
5
+ Sodium::Buffer.empty(size) do |buffer|
6
+ Sodium::FFI::Random.randombytes_buf(
7
+ buffer.to_str,
8
+ buffer.bytesize
9
+ )
10
+ end
11
+ end
12
+
13
+ def self.integer(max = 2 ** 32 - 1)
14
+ Sodium::FFI::Random.randombytes_uniform(max)
15
+ end
16
+ end
@@ -0,0 +1,65 @@
1
+ require 'sodium'
2
+
3
+ class Sodium::SecretBox
4
+ include Sodium::Delegate
5
+
6
+ def self.key
7
+ Sodium::Buffer.key self.implementation[:KEYBYTES]
8
+ end
9
+
10
+ def initialize(key)
11
+ @key = self.class._key(key)
12
+ end
13
+
14
+ def nonce
15
+ Sodium::Buffer.nonce self.implementation[:NONCEBYTES]
16
+ end
17
+
18
+ def secret_box(message, nonce)
19
+ message = self.class._message(message)
20
+ nonce = self.class._nonce(nonce)
21
+
22
+ Sodium::Buffer.empty(message.bytesize) do |ciphertext|
23
+ self.implementation.nacl(
24
+ ciphertext.to_str,
25
+ message.to_str,
26
+ message.to_str.bytesize,
27
+ nonce.to_str,
28
+ @key.to_str
29
+ ) or raise Sodium::CryptoError, 'failed to close the secret box'
30
+ end.unpad self.implementation[:BOXZEROBYTES]
31
+ end
32
+
33
+ def open(ciphertext, nonce)
34
+ ciphertext = self.class._ciphertext(ciphertext)
35
+ nonce = self.class._nonce(nonce)
36
+
37
+ Sodium::Buffer.empty(ciphertext.bytesize) do |message|
38
+ self.implementation.nacl_open(
39
+ message.to_str,
40
+ ciphertext.to_str,
41
+ ciphertext.to_str.bytesize,
42
+ nonce.to_str,
43
+ @key.to_str
44
+ ) or raise Sodium::CryptoError, 'failed to open the secret box'
45
+ end.unpad self.implementation[:ZEROBYTES]
46
+ end
47
+
48
+ private
49
+
50
+ def self._key(k)
51
+ Sodium::Buffer.new k, self.implementation[:KEYBYTES]
52
+ end
53
+
54
+ def self._message(m)
55
+ Sodium::Buffer.new(m).pad self.implementation[:ZEROBYTES]
56
+ end
57
+
58
+ def self._ciphertext(c)
59
+ Sodium::Buffer.new(c).pad self.implementation[:BOXZEROBYTES]
60
+ end
61
+
62
+ def self._nonce(n)
63
+ Sodium::Buffer.new n, self.implementation[:NONCEBYTES]
64
+ end
65
+ end