nuckle 0.1.1
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.
- checksums.yaml +7 -0
- data/LICENSE +13 -0
- data/README.md +82 -0
- data/lib/nuckle/box.rb +56 -0
- data/lib/nuckle/crypto_error.rb +5 -0
- data/lib/nuckle/internals/curve25519.rb +105 -0
- data/lib/nuckle/internals/poly1305.rb +70 -0
- data/lib/nuckle/internals/salsa20.rb +228 -0
- data/lib/nuckle/private_key.rb +26 -0
- data/lib/nuckle/public_key.rb +22 -0
- data/lib/nuckle/random.rb +11 -0
- data/lib/nuckle/secret_box.rb +83 -0
- data/lib/nuckle/util.rb +28 -0
- data/lib/nuckle/version.rb +5 -0
- data/lib/nuckle.rb +13 -0
- metadata +55 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 00a442ba9e5d4c70e543b9f2ec156248c283881fcf2bb774dc592534bc0a79dc
|
|
4
|
+
data.tar.gz: 478ebf814c22348331fcbbb5692f302425bca6cb4c48365c4508c8c4426f30ed
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a291010aeaa3fce0a12479f2a8b40e0fd089125d295f1890801683350a3ffb7ea34c03ca527ed6e63738d539e305d7d46dce5294adc58b1c0494e26bf5e19835
|
|
7
|
+
data.tar.gz: '0886502547f3628973eab880936d6bec563bb48807f39b959e7bf390433acffa67d63d30e456ed8f51a5ccb547eb1c1c3739f43f23f098581d241444d003c263'
|
data/LICENSE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Copyright (c) 2026, Patrik Wenger
|
|
2
|
+
|
|
3
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
4
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
5
|
+
copyright notice and this permission notice appear in all copies.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
8
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
9
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
10
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
11
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
12
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
13
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# 🤜 Nuckle
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/nuckle)
|
|
4
|
+
[](https://github.com/paddor/nuckle/actions/workflows/ci.yml)
|
|
5
|
+
|
|
6
|
+
NaCl for Knuckleheads — pure Ruby crypto primitives, no libsodium required.
|
|
7
|
+
|
|
8
|
+
## Is it any good?
|
|
9
|
+
|
|
10
|
+
No. See [SECURITY.md](SECURITY.md).
|
|
11
|
+
|
|
12
|
+
## ⚠️ Don't use this
|
|
13
|
+
|
|
14
|
+
Ruby's bignum arithmetic is not constant-time, so private keys leak
|
|
15
|
+
through timing side channels. Use
|
|
16
|
+
[rbnacl](https://github.com/RubyCrypto/rbnacl) for anything that matters.
|
|
17
|
+
|
|
18
|
+
## 🤷 Why does this exist?
|
|
19
|
+
|
|
20
|
+
- Zero-dependency development and CI environments
|
|
21
|
+
- Seeing how well Ruby's YJIT performs at crypto ("just for fun")
|
|
22
|
+
- Educational purposes
|
|
23
|
+
- Environments where installing libsodium is impractical
|
|
24
|
+
|
|
25
|
+
## Primitives
|
|
26
|
+
|
|
27
|
+
| Primitive | What it does |
|
|
28
|
+
|-----------|-------------|
|
|
29
|
+
| **Curve25519** | Elliptic-curve Diffie-Hellman (key agreement) |
|
|
30
|
+
| **XSalsa20** | Extended-nonce stream cipher (includes HSalsa20, Salsa20) |
|
|
31
|
+
| **Poly1305** | One-time message authenticator |
|
|
32
|
+
| **Box** | Public-key authenticated encryption (Curve25519-XSalsa20-Poly1305) |
|
|
33
|
+
| **SecretBox** | Symmetric authenticated encryption (XSalsa20-Poly1305) |
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
require "nuckle"
|
|
39
|
+
|
|
40
|
+
# Generate a keypair
|
|
41
|
+
sk = Nuckle::PrivateKey.generate
|
|
42
|
+
pk = sk.public_key
|
|
43
|
+
|
|
44
|
+
# Public-key encryption
|
|
45
|
+
alice = Nuckle::PrivateKey.generate
|
|
46
|
+
bob = Nuckle::PrivateKey.generate
|
|
47
|
+
nonce = Nuckle::Random.random_bytes(24)
|
|
48
|
+
|
|
49
|
+
box = Nuckle::Box.new(bob.public_key, alice)
|
|
50
|
+
ciphertext = box.encrypt(nonce, "hello")
|
|
51
|
+
|
|
52
|
+
box2 = Nuckle::Box.new(alice.public_key, bob)
|
|
53
|
+
plaintext = box2.decrypt(nonce, ciphertext)
|
|
54
|
+
# => "hello"
|
|
55
|
+
|
|
56
|
+
# Symmetric encryption
|
|
57
|
+
key = Nuckle::Random.random_bytes(32)
|
|
58
|
+
nonce = Nuckle::Random.random_bytes(24)
|
|
59
|
+
box = Nuckle::SecretBox.new(key)
|
|
60
|
+
|
|
61
|
+
ciphertext = box.encrypt(nonce, "hello")
|
|
62
|
+
plaintext = box.decrypt(nonce, ciphertext)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## API Compatibility
|
|
66
|
+
|
|
67
|
+
The API mirrors [rbnacl](https://github.com/RubyCrypto/rbnacl).
|
|
68
|
+
If you know what you're doing:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
RbNaCl = Nuckle
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Verification
|
|
75
|
+
|
|
76
|
+
All primitives are tested against rbnacl/libsodium test vectors
|
|
77
|
+
(NaCl distribution, RFC 7748, RFC 8439) and cross-validated to
|
|
78
|
+
produce byte-identical output.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
ISC
|
data/lib/nuckle/box.rb
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nuckle
|
|
4
|
+
# Public-key authenticated encryption: Curve25519-XSalsa20-Poly1305.
|
|
5
|
+
#
|
|
6
|
+
# Compatible with NaCl crypto_box / libsodium crypto_box_curve25519xsalsa20poly1305.
|
|
7
|
+
#
|
|
8
|
+
class Box
|
|
9
|
+
NONCEBYTES = 24
|
|
10
|
+
PUBLICKEYBYTES = 32
|
|
11
|
+
PRIVATEKEYBYTES = 32
|
|
12
|
+
BEFORENMBYTES = 32
|
|
13
|
+
MACBYTES = 16
|
|
14
|
+
|
|
15
|
+
def initialize(public_key, private_key)
|
|
16
|
+
pk = extract_bytes(public_key, PUBLICKEYBYTES, "public key")
|
|
17
|
+
sk = extract_bytes(private_key, PRIVATEKEYBYTES, "private key")
|
|
18
|
+
|
|
19
|
+
# Compute shared secret via Curve25519 DH
|
|
20
|
+
shared = Internals::Curve25519.scalarmult(sk, pk)
|
|
21
|
+
|
|
22
|
+
# Derive symmetric key via HSalsa20 (the "beforenm" step)
|
|
23
|
+
key = Internals::Salsa20.hsalsa20(shared, "\x00" * 16)
|
|
24
|
+
|
|
25
|
+
@secret_box = SecretBox.new(key)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def nonce_bytes = NONCEBYTES
|
|
29
|
+
|
|
30
|
+
# Encrypt plaintext with 24-byte nonce.
|
|
31
|
+
def encrypt(nonce, plaintext)
|
|
32
|
+
@secret_box.encrypt(nonce, plaintext)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Decrypt ciphertext with 24-byte nonce.
|
|
36
|
+
def decrypt(nonce, ciphertext)
|
|
37
|
+
@secret_box.decrypt(nonce, ciphertext)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
alias box encrypt
|
|
41
|
+
alias open decrypt
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def extract_bytes(key, expected_size, name)
|
|
46
|
+
bytes = case key
|
|
47
|
+
when PublicKey, PrivateKey then key.to_s
|
|
48
|
+
when String then key.b
|
|
49
|
+
else raise ArgumentError, "#{name} must be a Key or String"
|
|
50
|
+
end
|
|
51
|
+
raise ArgumentError, "#{name} must be #{expected_size} bytes (got #{bytes.bytesize})" unless bytes.bytesize == expected_size
|
|
52
|
+
|
|
53
|
+
bytes
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nuckle
|
|
4
|
+
module Internals
|
|
5
|
+
# Curve25519 elliptic-curve Diffie-Hellman.
|
|
6
|
+
#
|
|
7
|
+
# Montgomery curve: y^2 = x^3 + 486662*x^2 + x over GF(2^255 - 19).
|
|
8
|
+
# Uses the Montgomery ladder for scalar multiplication.
|
|
9
|
+
#
|
|
10
|
+
# Reference: RFC 7748
|
|
11
|
+
#
|
|
12
|
+
module Curve25519
|
|
13
|
+
P = (1 << 255) - 19
|
|
14
|
+
A24 = 121666 # (486662 + 2) / 4
|
|
15
|
+
BASE_U = 9
|
|
16
|
+
|
|
17
|
+
MASK64 = 0xFFFFFFFFFFFFFFFF
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# Scalar multiplication: compute scalar * point on Curve25519.
|
|
22
|
+
#
|
|
23
|
+
# @param scalar [String] 32-byte scalar (private key)
|
|
24
|
+
# @param u_bytes [String] 32-byte u-coordinate (public key / base point)
|
|
25
|
+
# @return [String] 32-byte result u-coordinate
|
|
26
|
+
#
|
|
27
|
+
def scalarmult(scalar, u_bytes)
|
|
28
|
+
k = decode_scalar(scalar)
|
|
29
|
+
u = decode_u(u_bytes)
|
|
30
|
+
|
|
31
|
+
x_1 = u
|
|
32
|
+
x_2 = 1
|
|
33
|
+
z_2 = 0
|
|
34
|
+
x_3 = u
|
|
35
|
+
z_3 = 1
|
|
36
|
+
|
|
37
|
+
swap = 0
|
|
38
|
+
|
|
39
|
+
254.downto(0) do |t|
|
|
40
|
+
k_t = (k >> t) & 1
|
|
41
|
+
swap ^= k_t
|
|
42
|
+
# Constant-time conditional swap (XOR mask)
|
|
43
|
+
mask = -swap
|
|
44
|
+
dummy = (x_2 ^ x_3) & mask; x_2 ^= dummy; x_3 ^= dummy
|
|
45
|
+
dummy = (z_2 ^ z_3) & mask; z_2 ^= dummy; z_3 ^= dummy
|
|
46
|
+
swap = k_t
|
|
47
|
+
|
|
48
|
+
a = (x_2 + z_2) % P
|
|
49
|
+
aa = (a * a) % P
|
|
50
|
+
b = (x_2 - z_2) % P
|
|
51
|
+
bb = (b * b) % P
|
|
52
|
+
e = (aa - bb) % P
|
|
53
|
+
c = (x_3 + z_3) % P
|
|
54
|
+
d = (x_3 - z_3) % P
|
|
55
|
+
da = (d * a) % P
|
|
56
|
+
cb = (c * b) % P
|
|
57
|
+
|
|
58
|
+
sum = (da + cb) % P; x_3 = (sum * sum) % P
|
|
59
|
+
dif = (da - cb) % P; z_3 = (x_1 * ((dif * dif) % P)) % P
|
|
60
|
+
x_2 = (aa * bb) % P
|
|
61
|
+
z_2 = (e * ((bb + A24 * e) % P)) % P
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Final cswap
|
|
65
|
+
mask = -swap
|
|
66
|
+
dummy = (x_2 ^ x_3) & mask; x_2 ^= dummy
|
|
67
|
+
dummy = (z_2 ^ z_3) & mask; z_2 ^= dummy
|
|
68
|
+
|
|
69
|
+
result = (x_2 * z_2.pow(P - 2, P)) % P
|
|
70
|
+
encode_u(result)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Scalar multiplication with the standard base point (u=9).
|
|
74
|
+
#
|
|
75
|
+
# @param scalar [String] 32-byte scalar (private key)
|
|
76
|
+
# @return [String] 32-byte public key
|
|
77
|
+
#
|
|
78
|
+
def scalarmult_base(scalar)
|
|
79
|
+
scalarmult(scalar, "\x09".b + "\x00".b * 31)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Decode a 32-byte scalar with clamping (per RFC 7748).
|
|
83
|
+
def decode_scalar(s)
|
|
84
|
+
buf = s.b.dup
|
|
85
|
+
buf.setbyte(0, buf.getbyte(0) & 248)
|
|
86
|
+
buf.setbyte(31, (buf.getbyte(31) & 127) | 64)
|
|
87
|
+
w = buf.unpack("Q<4")
|
|
88
|
+
w[0] | (w[1] << 64) | (w[2] << 128) | (w[3] << 192)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Decode a u-coordinate from 32 bytes (little-endian, mask high bit).
|
|
92
|
+
def decode_u(u_bytes)
|
|
93
|
+
buf = u_bytes.b.dup
|
|
94
|
+
buf.setbyte(31, buf.getbyte(31) & 0x7F)
|
|
95
|
+
w = buf.unpack("Q<4")
|
|
96
|
+
w[0] | (w[1] << 64) | (w[2] << 128) | (w[3] << 192)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Encode a field element to 32 bytes (little-endian).
|
|
100
|
+
def encode_u(u)
|
|
101
|
+
[u & MASK64, (u >> 64) & MASK64, (u >> 128) & MASK64, (u >> 192) & MASK64].pack("Q<4")
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nuckle
|
|
4
|
+
module Internals
|
|
5
|
+
# Poly1305 one-time message authenticator.
|
|
6
|
+
#
|
|
7
|
+
# Reference: https://cr.yp.to/mac/poly1305-20050329.pdf
|
|
8
|
+
# RFC 8439 Section 2.5
|
|
9
|
+
#
|
|
10
|
+
module Poly1305
|
|
11
|
+
P = (1 << 130) - 5
|
|
12
|
+
|
|
13
|
+
# Clamp mask for r (clear specific bits per spec)
|
|
14
|
+
R_CLAMP = 0x0ffffffc0ffffffc0ffffffc0fffffff
|
|
15
|
+
|
|
16
|
+
MASK128 = (1 << 128) - 1
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# Compute Poly1305 MAC.
|
|
21
|
+
#
|
|
22
|
+
# @param key [String] 32-byte one-time key (r || s)
|
|
23
|
+
# @param message [String] message to authenticate
|
|
24
|
+
# @return [String] 16-byte authentication tag
|
|
25
|
+
#
|
|
26
|
+
def mac(key, message)
|
|
27
|
+
r = le_bytes_to_int(key, 0, 16) & R_CLAMP
|
|
28
|
+
s = le_bytes_to_int(key, 16, 16)
|
|
29
|
+
|
|
30
|
+
h = 0
|
|
31
|
+
msg = message.b
|
|
32
|
+
len = msg.bytesize
|
|
33
|
+
off = 0
|
|
34
|
+
|
|
35
|
+
while off < len
|
|
36
|
+
remaining = len - off
|
|
37
|
+
take = remaining < 16 ? remaining : 16
|
|
38
|
+
block = le_bytes_to_int(msg, off, take)
|
|
39
|
+
|
|
40
|
+
# Add high bit (2^(8*take)) to mark block as non-zero-padded
|
|
41
|
+
block |= 1 << (take * 8)
|
|
42
|
+
|
|
43
|
+
h = ((h + block) * r) % P
|
|
44
|
+
off += take
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Final: (h + s) mod 2^128
|
|
48
|
+
tag = (h + s) & MASK128
|
|
49
|
+
int_to_le_bytes(tag)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Read little-endian integer from a string at offset, length bytes.
|
|
53
|
+
def le_bytes_to_int(bytes, offset, length)
|
|
54
|
+
if length == 16
|
|
55
|
+
lo, hi = bytes.unpack("@#{offset}Q<2")
|
|
56
|
+
lo | (hi << 64)
|
|
57
|
+
else
|
|
58
|
+
n = 0
|
|
59
|
+
length.times { |i| n |= bytes.getbyte(offset + i) << (8 * i) }
|
|
60
|
+
n
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Write a 128-bit integer as 16 little-endian bytes.
|
|
65
|
+
def int_to_le_bytes(n)
|
|
66
|
+
[n & 0xFFFFFFFFFFFFFFFF, (n >> 64) & 0xFFFFFFFFFFFFFFFF].pack("Q<2")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nuckle
|
|
4
|
+
module Internals
|
|
5
|
+
# Salsa20 stream cipher family: Salsa20 core, HSalsa20, XSalsa20.
|
|
6
|
+
#
|
|
7
|
+
# References:
|
|
8
|
+
# - https://cr.yp.to/snuffle/spec.pdf (Salsa20 specification)
|
|
9
|
+
# - https://cr.yp.to/snuffle/xsalsa-20110204.pdf (XSalsa20)
|
|
10
|
+
#
|
|
11
|
+
module Salsa20
|
|
12
|
+
MASK32 = 0xFFFFFFFF
|
|
13
|
+
SIGMA = "expand 32-byte k".b.freeze
|
|
14
|
+
SIGMA_V4 = SIGMA.unpack("V4").freeze
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# Salsa20 core function: 20 rounds on 16 x 32-bit words.
|
|
19
|
+
# Returns the 64-byte output block.
|
|
20
|
+
#
|
|
21
|
+
# @param input [String] 64-byte input block
|
|
22
|
+
# @return [String] 64-byte output block
|
|
23
|
+
#
|
|
24
|
+
def core(input)
|
|
25
|
+
x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15 =
|
|
26
|
+
input.unpack("V16")
|
|
27
|
+
|
|
28
|
+
z0 = x0; z1 = x1; z2 = x2; z3 = x3
|
|
29
|
+
z4 = x4; z5 = x5; z6 = x6; z7 = x7
|
|
30
|
+
z8 = x8; z9 = x9; z10 = x10; z11 = x11
|
|
31
|
+
z12 = x12; z13 = x13; z14 = x14; z15 = x15
|
|
32
|
+
|
|
33
|
+
10.times do
|
|
34
|
+
# Column rounds
|
|
35
|
+
t = (z0 + z12) & MASK32; z4 ^= ((t << 7) | (t >> 25)) & MASK32
|
|
36
|
+
t = (z4 + z0) & MASK32; z8 ^= ((t << 9) | (t >> 23)) & MASK32
|
|
37
|
+
t = (z8 + z4) & MASK32; z12 ^= ((t << 13) | (t >> 19)) & MASK32
|
|
38
|
+
t = (z12 + z8) & MASK32; z0 ^= ((t << 18) | (t >> 14)) & MASK32
|
|
39
|
+
|
|
40
|
+
t = (z5 + z1) & MASK32; z9 ^= ((t << 7) | (t >> 25)) & MASK32
|
|
41
|
+
t = (z9 + z5) & MASK32; z13 ^= ((t << 9) | (t >> 23)) & MASK32
|
|
42
|
+
t = (z13 + z9) & MASK32; z1 ^= ((t << 13) | (t >> 19)) & MASK32
|
|
43
|
+
t = (z1 + z13) & MASK32; z5 ^= ((t << 18) | (t >> 14)) & MASK32
|
|
44
|
+
|
|
45
|
+
t = (z10 + z6) & MASK32; z14 ^= ((t << 7) | (t >> 25)) & MASK32
|
|
46
|
+
t = (z14 + z10) & MASK32; z2 ^= ((t << 9) | (t >> 23)) & MASK32
|
|
47
|
+
t = (z2 + z14) & MASK32; z6 ^= ((t << 13) | (t >> 19)) & MASK32
|
|
48
|
+
t = (z6 + z2) & MASK32; z10 ^= ((t << 18) | (t >> 14)) & MASK32
|
|
49
|
+
|
|
50
|
+
t = (z15 + z11) & MASK32; z3 ^= ((t << 7) | (t >> 25)) & MASK32
|
|
51
|
+
t = (z3 + z15) & MASK32; z7 ^= ((t << 9) | (t >> 23)) & MASK32
|
|
52
|
+
t = (z7 + z3) & MASK32; z11 ^= ((t << 13) | (t >> 19)) & MASK32
|
|
53
|
+
t = (z11 + z7) & MASK32; z15 ^= ((t << 18) | (t >> 14)) & MASK32
|
|
54
|
+
|
|
55
|
+
# Row rounds
|
|
56
|
+
t = (z0 + z3) & MASK32; z1 ^= ((t << 7) | (t >> 25)) & MASK32
|
|
57
|
+
t = (z1 + z0) & MASK32; z2 ^= ((t << 9) | (t >> 23)) & MASK32
|
|
58
|
+
t = (z2 + z1) & MASK32; z3 ^= ((t << 13) | (t >> 19)) & MASK32
|
|
59
|
+
t = (z3 + z2) & MASK32; z0 ^= ((t << 18) | (t >> 14)) & MASK32
|
|
60
|
+
|
|
61
|
+
t = (z5 + z4) & MASK32; z6 ^= ((t << 7) | (t >> 25)) & MASK32
|
|
62
|
+
t = (z6 + z5) & MASK32; z7 ^= ((t << 9) | (t >> 23)) & MASK32
|
|
63
|
+
t = (z7 + z6) & MASK32; z4 ^= ((t << 13) | (t >> 19)) & MASK32
|
|
64
|
+
t = (z4 + z7) & MASK32; z5 ^= ((t << 18) | (t >> 14)) & MASK32
|
|
65
|
+
|
|
66
|
+
t = (z10 + z9) & MASK32; z11 ^= ((t << 7) | (t >> 25)) & MASK32
|
|
67
|
+
t = (z11 + z10) & MASK32; z8 ^= ((t << 9) | (t >> 23)) & MASK32
|
|
68
|
+
t = (z8 + z11) & MASK32; z9 ^= ((t << 13) | (t >> 19)) & MASK32
|
|
69
|
+
t = (z9 + z8) & MASK32; z10 ^= ((t << 18) | (t >> 14)) & MASK32
|
|
70
|
+
|
|
71
|
+
t = (z15 + z14) & MASK32; z12 ^= ((t << 7) | (t >> 25)) & MASK32
|
|
72
|
+
t = (z12 + z15) & MASK32; z13 ^= ((t << 9) | (t >> 23)) & MASK32
|
|
73
|
+
t = (z13 + z12) & MASK32; z14 ^= ((t << 13) | (t >> 19)) & MASK32
|
|
74
|
+
t = (z14 + z13) & MASK32; z15 ^= ((t << 18) | (t >> 14)) & MASK32
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
[
|
|
78
|
+
(z0 + x0) & MASK32, (z1 + x1) & MASK32, (z2 + x2) & MASK32, (z3 + x3) & MASK32,
|
|
79
|
+
(z4 + x4) & MASK32, (z5 + x5) & MASK32, (z6 + x6) & MASK32, (z7 + x7) & MASK32,
|
|
80
|
+
(z8 + x8) & MASK32, (z9 + x9) & MASK32, (z10 + x10) & MASK32, (z11 + x11) & MASK32,
|
|
81
|
+
(z12 + x12) & MASK32, (z13 + x13) & MASK32, (z14 + x14) & MASK32, (z15 + x15) & MASK32,
|
|
82
|
+
].pack("V16")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# HSalsa20: derives a 32-byte subkey from a 32-byte key and 16-byte nonce.
|
|
86
|
+
# Returns words [0,5,10,15,6,7,8,9] from the NON-added state.
|
|
87
|
+
#
|
|
88
|
+
# @param key [String] 32-byte key
|
|
89
|
+
# @param nonce [String] 16-byte nonce
|
|
90
|
+
# @return [String] 32-byte subkey
|
|
91
|
+
#
|
|
92
|
+
def hsalsa20(key, nonce)
|
|
93
|
+
k0, k1, k2, k3, k4, k5, k6, k7 = key.unpack("V8")
|
|
94
|
+
n0, n1, n2, n3 = nonce.unpack("V4")
|
|
95
|
+
s0, s1, s2, s3 = SIGMA_V4
|
|
96
|
+
|
|
97
|
+
z0 = s0; z1 = k0; z2 = k1; z3 = k2
|
|
98
|
+
z4 = k3; z5 = s1; z6 = n0; z7 = n1
|
|
99
|
+
z8 = n2; z9 = n3; z10 = s2; z11 = k4
|
|
100
|
+
z12 = k5; z13 = k6; z14 = k7; z15 = s3
|
|
101
|
+
|
|
102
|
+
10.times do
|
|
103
|
+
t = (z0 + z12) & MASK32; z4 ^= ((t << 7) | (t >> 25)) & MASK32
|
|
104
|
+
t = (z4 + z0) & MASK32; z8 ^= ((t << 9) | (t >> 23)) & MASK32
|
|
105
|
+
t = (z8 + z4) & MASK32; z12 ^= ((t << 13) | (t >> 19)) & MASK32
|
|
106
|
+
t = (z12 + z8) & MASK32; z0 ^= ((t << 18) | (t >> 14)) & MASK32
|
|
107
|
+
|
|
108
|
+
t = (z5 + z1) & MASK32; z9 ^= ((t << 7) | (t >> 25)) & MASK32
|
|
109
|
+
t = (z9 + z5) & MASK32; z13 ^= ((t << 9) | (t >> 23)) & MASK32
|
|
110
|
+
t = (z13 + z9) & MASK32; z1 ^= ((t << 13) | (t >> 19)) & MASK32
|
|
111
|
+
t = (z1 + z13) & MASK32; z5 ^= ((t << 18) | (t >> 14)) & MASK32
|
|
112
|
+
|
|
113
|
+
t = (z10 + z6) & MASK32; z14 ^= ((t << 7) | (t >> 25)) & MASK32
|
|
114
|
+
t = (z14 + z10) & MASK32; z2 ^= ((t << 9) | (t >> 23)) & MASK32
|
|
115
|
+
t = (z2 + z14) & MASK32; z6 ^= ((t << 13) | (t >> 19)) & MASK32
|
|
116
|
+
t = (z6 + z2) & MASK32; z10 ^= ((t << 18) | (t >> 14)) & MASK32
|
|
117
|
+
|
|
118
|
+
t = (z15 + z11) & MASK32; z3 ^= ((t << 7) | (t >> 25)) & MASK32
|
|
119
|
+
t = (z3 + z15) & MASK32; z7 ^= ((t << 9) | (t >> 23)) & MASK32
|
|
120
|
+
t = (z7 + z3) & MASK32; z11 ^= ((t << 13) | (t >> 19)) & MASK32
|
|
121
|
+
t = (z11 + z7) & MASK32; z15 ^= ((t << 18) | (t >> 14)) & MASK32
|
|
122
|
+
|
|
123
|
+
t = (z0 + z3) & MASK32; z1 ^= ((t << 7) | (t >> 25)) & MASK32
|
|
124
|
+
t = (z1 + z0) & MASK32; z2 ^= ((t << 9) | (t >> 23)) & MASK32
|
|
125
|
+
t = (z2 + z1) & MASK32; z3 ^= ((t << 13) | (t >> 19)) & MASK32
|
|
126
|
+
t = (z3 + z2) & MASK32; z0 ^= ((t << 18) | (t >> 14)) & MASK32
|
|
127
|
+
|
|
128
|
+
t = (z5 + z4) & MASK32; z6 ^= ((t << 7) | (t >> 25)) & MASK32
|
|
129
|
+
t = (z6 + z5) & MASK32; z7 ^= ((t << 9) | (t >> 23)) & MASK32
|
|
130
|
+
t = (z7 + z6) & MASK32; z4 ^= ((t << 13) | (t >> 19)) & MASK32
|
|
131
|
+
t = (z4 + z7) & MASK32; z5 ^= ((t << 18) | (t >> 14)) & MASK32
|
|
132
|
+
|
|
133
|
+
t = (z10 + z9) & MASK32; z11 ^= ((t << 7) | (t >> 25)) & MASK32
|
|
134
|
+
t = (z11 + z10) & MASK32; z8 ^= ((t << 9) | (t >> 23)) & MASK32
|
|
135
|
+
t = (z8 + z11) & MASK32; z9 ^= ((t << 13) | (t >> 19)) & MASK32
|
|
136
|
+
t = (z9 + z8) & MASK32; z10 ^= ((t << 18) | (t >> 14)) & MASK32
|
|
137
|
+
|
|
138
|
+
t = (z15 + z14) & MASK32; z12 ^= ((t << 7) | (t >> 25)) & MASK32
|
|
139
|
+
t = (z12 + z15) & MASK32; z13 ^= ((t << 9) | (t >> 23)) & MASK32
|
|
140
|
+
t = (z13 + z12) & MASK32; z14 ^= ((t << 13) | (t >> 19)) & MASK32
|
|
141
|
+
t = (z14 + z13) & MASK32; z15 ^= ((t << 18) | (t >> 14)) & MASK32
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Return words [0,5,10,15,6,7,8,9] — the diagonal + second row
|
|
145
|
+
[z0, z5, z10, z15, z6, z7, z8, z9].pack("V8")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# XSalsa20 stream XOR: encrypts/decrypts message using 32-byte key and 24-byte nonce.
|
|
149
|
+
#
|
|
150
|
+
# @param key [String] 32-byte key
|
|
151
|
+
# @param nonce [String] 24-byte nonce
|
|
152
|
+
# @param message [String] plaintext/ciphertext
|
|
153
|
+
# @return [String] XOR'd output (same length as message)
|
|
154
|
+
#
|
|
155
|
+
def xsalsa20_xor(key, nonce, message)
|
|
156
|
+
subkey = hsalsa20(key, nonce.byteslice(0, 16))
|
|
157
|
+
sub_nonce = (nonce.byteslice(16, 8) + "\x00\x00\x00\x00\x00\x00\x00\x00").b
|
|
158
|
+
salsa20_xor(subkey, sub_nonce, message)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Salsa20 stream XOR with 32-byte key and 16-byte nonce/counter.
|
|
162
|
+
#
|
|
163
|
+
# @param key [String] 32-byte key
|
|
164
|
+
# @param nonce [String] 16-byte (8-byte nonce + 8-byte counter, LE)
|
|
165
|
+
# @param message [String] plaintext/ciphertext
|
|
166
|
+
# @return [String] XOR'd output
|
|
167
|
+
#
|
|
168
|
+
def salsa20_xor(key, nonce, message)
|
|
169
|
+
k0, k1, k2, k3, k4, k5, k6, k7 = key.unpack("V8")
|
|
170
|
+
n0, n1, n2, n3 = nonce.unpack("V4")
|
|
171
|
+
s0, s1, s2, s3 = SIGMA_V4
|
|
172
|
+
|
|
173
|
+
msg = message.b
|
|
174
|
+
len = msg.bytesize
|
|
175
|
+
out = String.new(capacity: len, encoding: Encoding::BINARY)
|
|
176
|
+
offset = 0
|
|
177
|
+
|
|
178
|
+
while offset < len
|
|
179
|
+
# Build + run Salsa20 core inline
|
|
180
|
+
block = core([
|
|
181
|
+
s0, k0, k1, k2,
|
|
182
|
+
k3, s1, n0, n1,
|
|
183
|
+
n2, n3, s2, k4,
|
|
184
|
+
k5, k6, k7, s3,
|
|
185
|
+
].pack("V16"))
|
|
186
|
+
|
|
187
|
+
# XOR message bytes with keystream block
|
|
188
|
+
remaining = len - offset
|
|
189
|
+
take = remaining < 64 ? remaining : 64
|
|
190
|
+
|
|
191
|
+
if take == 64
|
|
192
|
+
# Fast path: XOR 8 x uint64
|
|
193
|
+
m = msg.byteslice(offset, 64).unpack("Q<8")
|
|
194
|
+
b = block.unpack("Q<8")
|
|
195
|
+
out << [m[0] ^ b[0], m[1] ^ b[1], m[2] ^ b[2], m[3] ^ b[3],
|
|
196
|
+
m[4] ^ b[4], m[5] ^ b[5], m[6] ^ b[6], m[7] ^ b[7]].pack("Q<8")
|
|
197
|
+
else
|
|
198
|
+
# Tail: XOR byte-by-byte
|
|
199
|
+
m = msg.byteslice(offset, take).unpack("C*")
|
|
200
|
+
b = block.unpack("C*")
|
|
201
|
+
take.times { |i| m[i] ^= b[i] }
|
|
202
|
+
out << m.pack("C*")
|
|
203
|
+
end
|
|
204
|
+
offset += take
|
|
205
|
+
|
|
206
|
+
# Increment 64-bit counter (words n2, n3)
|
|
207
|
+
counter = n2 | (n3 << 32)
|
|
208
|
+
counter += 1
|
|
209
|
+
n2 = counter & MASK32
|
|
210
|
+
n3 = (counter >> 32) & MASK32
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
out
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Generate XSalsa20 keystream (no XOR, just the stream bytes).
|
|
217
|
+
#
|
|
218
|
+
# @param key [String] 32-byte key
|
|
219
|
+
# @param nonce [String] 24-byte nonce
|
|
220
|
+
# @param length [Integer] number of bytes to generate
|
|
221
|
+
# @return [String] keystream bytes
|
|
222
|
+
#
|
|
223
|
+
def xsalsa20_stream(key, nonce, length)
|
|
224
|
+
xsalsa20_xor(key, nonce, "\x00".b * length)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nuckle
|
|
4
|
+
class PrivateKey
|
|
5
|
+
BYTES = 32
|
|
6
|
+
|
|
7
|
+
def self.generate
|
|
8
|
+
new(Random.random_bytes(BYTES))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(key)
|
|
12
|
+
key = key.to_s if key.respond_to?(:to_s) && !key.is_a?(String)
|
|
13
|
+
key = key.b
|
|
14
|
+
raise ArgumentError, "private key must be #{BYTES} bytes (got #{key.bytesize})" unless key.bytesize == BYTES
|
|
15
|
+
|
|
16
|
+
@key = key
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def public_key
|
|
20
|
+
PublicKey.new(Internals::Curve25519.scalarmult_base(@key))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_bytes = @key
|
|
24
|
+
def to_s = @key
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nuckle
|
|
4
|
+
class PublicKey
|
|
5
|
+
BYTES = 32
|
|
6
|
+
|
|
7
|
+
def initialize(key)
|
|
8
|
+
key = key.to_s if key.respond_to?(:to_s) && !key.is_a?(String)
|
|
9
|
+
key = key.b
|
|
10
|
+
raise ArgumentError, "public key must be #{BYTES} bytes (got #{key.bytesize})" unless key.bytesize == BYTES
|
|
11
|
+
|
|
12
|
+
@key = key
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_bytes = @key
|
|
16
|
+
def to_s = @key
|
|
17
|
+
|
|
18
|
+
def ==(other)
|
|
19
|
+
other.is_a?(PublicKey) && Util.verify32(@key, other.to_s)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nuckle
|
|
4
|
+
# Symmetric authenticated encryption: XSalsa20-Poly1305.
|
|
5
|
+
#
|
|
6
|
+
# Compatible with NaCl crypto_secretbox / libsodium crypto_secretbox_xsalsa20poly1305.
|
|
7
|
+
#
|
|
8
|
+
class SecretBox
|
|
9
|
+
KEYBYTES = 32
|
|
10
|
+
NONCEBYTES = 24
|
|
11
|
+
ZEROBYTES = 32
|
|
12
|
+
BOXZEROBYTES = 16
|
|
13
|
+
MACBYTES = 16
|
|
14
|
+
|
|
15
|
+
def initialize(key)
|
|
16
|
+
key = key.b
|
|
17
|
+
raise ArgumentError, "key must be #{KEYBYTES} bytes (got #{key.bytesize})" unless key.bytesize == KEYBYTES
|
|
18
|
+
|
|
19
|
+
@key = key
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def nonce_bytes = NONCEBYTES
|
|
23
|
+
def key_bytes = KEYBYTES
|
|
24
|
+
|
|
25
|
+
# Encrypt plaintext with 24-byte nonce.
|
|
26
|
+
#
|
|
27
|
+
# @param nonce [String] 24-byte nonce
|
|
28
|
+
# @param plaintext [String]
|
|
29
|
+
# @return [String] authenticator (16 bytes) + ciphertext
|
|
30
|
+
#
|
|
31
|
+
def encrypt(nonce, plaintext)
|
|
32
|
+
nonce = nonce.b
|
|
33
|
+
plaintext = plaintext.b
|
|
34
|
+
raise ArgumentError, "nonce must be #{NONCEBYTES} bytes" unless nonce.bytesize == NONCEBYTES
|
|
35
|
+
|
|
36
|
+
# Pad with 32 zero bytes
|
|
37
|
+
padded = ("\x00" * ZEROBYTES + plaintext).b
|
|
38
|
+
c = Internals::Salsa20.xsalsa20_xor(@key, nonce, padded)
|
|
39
|
+
|
|
40
|
+
# First 32 bytes of c: bytes 0..31 of XSalsa20 keystream XOR'd with zeros
|
|
41
|
+
# → bytes 0..31 ARE the keystream. Use first 32 as Poly1305 one-time key.
|
|
42
|
+
poly_key = c.byteslice(0, ZEROBYTES)
|
|
43
|
+
mac = Internals::Poly1305.mac(poly_key, c.byteslice(ZEROBYTES..))
|
|
44
|
+
|
|
45
|
+
# Return: 16-byte MAC + ciphertext (skip first 16 zero bytes of c)
|
|
46
|
+
# Actually NaCl convention: c[0..15] are zeros, c[16..31] replaced by mac
|
|
47
|
+
mac + c.byteslice(ZEROBYTES..)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Decrypt ciphertext with 24-byte nonce.
|
|
51
|
+
#
|
|
52
|
+
# @param nonce [String] 24-byte nonce
|
|
53
|
+
# @param ciphertext [String] authenticator (16 bytes) + ciphertext
|
|
54
|
+
# @return [String] plaintext
|
|
55
|
+
# @raise [CryptoError] on authentication failure
|
|
56
|
+
#
|
|
57
|
+
def decrypt(nonce, ciphertext)
|
|
58
|
+
nonce = nonce.b
|
|
59
|
+
ciphertext = ciphertext.b
|
|
60
|
+
raise ArgumentError, "nonce must be #{NONCEBYTES} bytes" unless nonce.bytesize == NONCEBYTES
|
|
61
|
+
raise CryptoError, "ciphertext too short" if ciphertext.bytesize < MACBYTES
|
|
62
|
+
|
|
63
|
+
mac = ciphertext.byteslice(0, MACBYTES)
|
|
64
|
+
ct = ciphertext.byteslice(MACBYTES..)
|
|
65
|
+
|
|
66
|
+
# Generate Poly1305 key from first 32 bytes of XSalsa20 keystream
|
|
67
|
+
poly_key = Internals::Salsa20.xsalsa20_stream(@key, nonce, ZEROBYTES)
|
|
68
|
+
|
|
69
|
+
# Verify MAC
|
|
70
|
+
expected_mac = Internals::Poly1305.mac(poly_key, ct)
|
|
71
|
+
raise CryptoError, "decryption failed" unless Util.verify16(mac, expected_mac)
|
|
72
|
+
|
|
73
|
+
# Decrypt
|
|
74
|
+
padded = ("\x00" * ZEROBYTES + ct).b
|
|
75
|
+
m = Internals::Salsa20.xsalsa20_xor(@key, nonce, padded)
|
|
76
|
+
m.byteslice(ZEROBYTES..)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Aliases matching rbnacl API
|
|
80
|
+
alias box encrypt
|
|
81
|
+
alias open decrypt
|
|
82
|
+
end
|
|
83
|
+
end
|
data/lib/nuckle/util.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nuckle
|
|
4
|
+
module Util
|
|
5
|
+
# Compare two 16-byte strings.
|
|
6
|
+
def self.verify16(a, b)
|
|
7
|
+
verify(a, b, 16)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Compare two 32-byte strings.
|
|
11
|
+
def self.verify32(a, b)
|
|
12
|
+
verify(a, b, 32)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Compare two 64-byte strings.
|
|
16
|
+
def self.verify64(a, b)
|
|
17
|
+
verify(a, b, 64)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.verify(a, b, expected_size = nil)
|
|
21
|
+
a = a.b if a.encoding != Encoding::BINARY
|
|
22
|
+
b = b.b if b.encoding != Encoding::BINARY
|
|
23
|
+
return false if expected_size && (a.bytesize != expected_size || b.bytesize != expected_size)
|
|
24
|
+
|
|
25
|
+
a == b
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/nuckle.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "nuckle/version"
|
|
4
|
+
require_relative "nuckle/crypto_error"
|
|
5
|
+
require_relative "nuckle/random"
|
|
6
|
+
require_relative "nuckle/util"
|
|
7
|
+
require_relative "nuckle/internals/curve25519"
|
|
8
|
+
require_relative "nuckle/internals/salsa20"
|
|
9
|
+
require_relative "nuckle/internals/poly1305"
|
|
10
|
+
require_relative "nuckle/public_key"
|
|
11
|
+
require_relative "nuckle/private_key"
|
|
12
|
+
require_relative "nuckle/box"
|
|
13
|
+
require_relative "nuckle/secret_box"
|
metadata
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: nuckle
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Patrik Wenger
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Pure Ruby implementation of the NaCl crypto primitives (Curve25519, XSalsa20,
|
|
13
|
+
Poly1305). No C extensions, no FFI, no libsodium. Is it any good? No.
|
|
14
|
+
email:
|
|
15
|
+
- paddor@gmail.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- LICENSE
|
|
21
|
+
- README.md
|
|
22
|
+
- lib/nuckle.rb
|
|
23
|
+
- lib/nuckle/box.rb
|
|
24
|
+
- lib/nuckle/crypto_error.rb
|
|
25
|
+
- lib/nuckle/internals/curve25519.rb
|
|
26
|
+
- lib/nuckle/internals/poly1305.rb
|
|
27
|
+
- lib/nuckle/internals/salsa20.rb
|
|
28
|
+
- lib/nuckle/private_key.rb
|
|
29
|
+
- lib/nuckle/public_key.rb
|
|
30
|
+
- lib/nuckle/random.rb
|
|
31
|
+
- lib/nuckle/secret_box.rb
|
|
32
|
+
- lib/nuckle/util.rb
|
|
33
|
+
- lib/nuckle/version.rb
|
|
34
|
+
homepage: https://github.com/paddor/nuckle
|
|
35
|
+
licenses:
|
|
36
|
+
- ISC
|
|
37
|
+
metadata: {}
|
|
38
|
+
rdoc_options: []
|
|
39
|
+
require_paths:
|
|
40
|
+
- lib
|
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '3.3'
|
|
46
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
47
|
+
requirements:
|
|
48
|
+
- - ">="
|
|
49
|
+
- !ruby/object:Gem::Version
|
|
50
|
+
version: '0'
|
|
51
|
+
requirements: []
|
|
52
|
+
rubygems_version: 4.0.6
|
|
53
|
+
specification_version: 4
|
|
54
|
+
summary: Pure Ruby NaCl crypto primitives
|
|
55
|
+
test_files: []
|