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.
- data/.gitignore +4 -0
- data/.travis.yml +27 -0
- data/Gemfile +9 -0
- data/README.md +111 -0
- data/Rakefile +3 -0
- data/VERSION +1 -0
- data/config/nacl_ffi.yml +90 -0
- data/lib/sodium.rb +24 -0
- data/lib/sodium/auth.rb +52 -0
- data/lib/sodium/box.rb +127 -0
- data/lib/sodium/buffer.rb +141 -0
- data/lib/sodium/delegate.rb +58 -0
- data/lib/sodium/ffi.rb +9 -0
- data/lib/sodium/ffi/crypto.rb +106 -0
- data/lib/sodium/ffi/lib_c.rb +9 -0
- data/lib/sodium/ffi/random.rb +11 -0
- data/lib/sodium/hash.rb +23 -0
- data/lib/sodium/one_time_auth.rb +52 -0
- data/lib/sodium/random.rb +16 -0
- data/lib/sodium/secret_box.rb +65 -0
- data/lib/sodium/sign.rb +75 -0
- data/lib/sodium/version.rb +5 -0
- data/sodium.gemspec +9 -3
- data/sodium.pub.gpg +37 -0
- data/tasks/libsodium.rake +70 -0
- data/tasks/test.rake +6 -0
- data/tasks/version.rake +3 -0
- data/test/sodium/auth/hmacsha256_test.rb +54 -0
- data/test/sodium/auth/hmacsha512256_test.rb +53 -0
- data/test/sodium/auth_test.rb +49 -0
- data/test/sodium/box/curve25519xsalsa20poly1305_test.rb +79 -0
- data/test/sodium/box_test.rb +109 -0
- data/test/sodium/buffer_test.rb +120 -0
- data/test/sodium/delegate_test.rb +44 -0
- data/test/sodium/hash/sha256_test.rb +31 -0
- data/test/sodium/hash/sha512_test.rb +35 -0
- data/test/sodium/hash_test.rb +35 -0
- data/test/sodium/one_time_auth/poly1305_test.rb +54 -0
- data/test/sodium/one_time_auth_test.rb +49 -0
- data/test/sodium/random_test.rb +25 -0
- data/test/sodium/secret_box/xsalsa20poly1305_test.rb +50 -0
- data/test/sodium/secret_box_test.rb +56 -0
- data/test/sodium/sign/ed25519_test.rb +52 -0
- data/test/sodium/sign_test.rb +58 -0
- data/test/test_helper.rb +44 -0
- metadata +117 -8
- 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,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,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
|
data/lib/sodium/hash.rb
ADDED
@@ -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
|