Sodium::SecureBuffer is now a Crypto::Secret
This commit is contained in:
parent
cd0ce1ccdb
commit
fca40d7764
@ -10,6 +10,10 @@ targets:
|
|||||||
main: examples/blake2b_hash.cr
|
main: examples/blake2b_hash.cr
|
||||||
pwhash_selector:
|
pwhash_selector:
|
||||||
main: examples/pwhash_selector.cr
|
main: examples/pwhash_selector.cr
|
||||||
|
dependencies:
|
||||||
|
crypto-secret:
|
||||||
|
github: didactic-drunk/crypto-secret.cr
|
||||||
|
branch: main
|
||||||
libraries:
|
libraries:
|
||||||
libsodium: ">= 1.0.18"
|
libsodium: ">= 1.0.18"
|
||||||
license: MIT
|
license: MIT
|
||||||
|
@ -7,9 +7,11 @@ end
|
|||||||
describe Sodium::SecureBuffer do
|
describe Sodium::SecureBuffer do
|
||||||
it "allocates empty" do
|
it "allocates empty" do
|
||||||
buf = Sodium::SecureBuffer.new 5
|
buf = Sodium::SecureBuffer.new 5
|
||||||
buf.to_slice.each do |b|
|
buf.readonly do |slice|
|
||||||
|
slice.each do |b|
|
||||||
b.should eq 0xdb_u8
|
b.should eq 0xdb_u8
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
buf.noaccess
|
buf.noaccess
|
||||||
buf.readonly
|
buf.readonly
|
||||||
@ -17,19 +19,23 @@ describe Sodium::SecureBuffer do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it "allocates random" do
|
it "allocates random" do
|
||||||
buf = Sodium::SecureBuffer.random 5
|
buf1 = Sodium::SecureBuffer.random 5
|
||||||
buf.to_slice.bytesize.should eq 5
|
buf2 = Sodium::SecureBuffer.random 5
|
||||||
buf.wipe
|
(buf1 == buf2).should be_false
|
||||||
|
buf1.wipe
|
||||||
end
|
end
|
||||||
|
|
||||||
it "copies and erases" do
|
it "copies and erases" do
|
||||||
bytes = Bytes.new(5) { 1_u8 }
|
bytes = Bytes.new(5) { 1_u8 }
|
||||||
|
|
||||||
buf = Sodium::SecureBuffer.new bytes, erase: true
|
buf = Sodium::SecureBuffer.new bytes, erase: true
|
||||||
buf.to_slice.bytesize.should eq 5
|
buf.readonly do |slice|
|
||||||
buf.to_slice.each do |b|
|
slice.bytesize.should eq 5
|
||||||
|
|
||||||
|
slice.each do |b|
|
||||||
b.should eq 1_u8
|
b.should eq 1_u8
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
bytes.to_slice.each do |b|
|
bytes.to_slice.each do |b|
|
||||||
b.should eq 0_u8
|
b.should eq 0_u8
|
||||||
@ -37,16 +43,21 @@ describe Sodium::SecureBuffer do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it "dups without crashing" do
|
it "dups without crashing" do
|
||||||
buf = Sodium::SecureBuffer.new 5
|
buf1 = Sodium::SecureBuffer.new 5
|
||||||
buf.readwrite
|
buf1.noaccess
|
||||||
|
|
||||||
buf2 = buf.dup
|
buf2 = buf1.dup
|
||||||
buf2.@state.should eq Sodium::SecureBuffer::State::Readwrite
|
buf2.@state.should eq Sodium::SecureBuffer::State::Noaccess
|
||||||
|
|
||||||
buf[0] = 1_u8
|
buf1.readwrite do |slice|
|
||||||
buf.to_slice.hexstring.should_not eq buf2.to_slice.hexstring
|
slice[0] = 1_u8
|
||||||
buf2[0] = 1_u8
|
end
|
||||||
buf.to_slice.hexstring.should eq buf2.to_slice.hexstring
|
buf1.hexstring.should_not eq buf2.hexstring
|
||||||
|
|
||||||
|
buf2.readwrite do |slice|
|
||||||
|
slice[0] = 1_u8
|
||||||
|
end
|
||||||
|
buf1.hexstring.should eq buf2.hexstring
|
||||||
end
|
end
|
||||||
|
|
||||||
it "transitions correctly" do
|
it "transitions correctly" do
|
||||||
|
@ -79,11 +79,13 @@ module Sodium
|
|||||||
end
|
end
|
||||||
|
|
||||||
subkey = SecureBuffer.new subkey_size
|
subkey = SecureBuffer.new subkey_size
|
||||||
|
subkey.readwrite do |sub_slice|
|
||||||
@sbuf.readonly do
|
@sbuf.readonly do
|
||||||
if (ret = LibSodium.crypto_kdf_derive_from_key(subkey, subkey.bytesize, subkey_id, context, self.to_slice)) != 0
|
if (ret = LibSodium.crypto_kdf_derive_from_key(sub_slice, sub_slice.bytesize, subkey_id, context, self.to_slice)) != 0
|
||||||
raise Sodium::Error.new("crypto_kdf_derive_from_key returned #{ret} (subkey size is probably out of range)")
|
raise Sodium::Error.new("crypto_kdf_derive_from_key returned #{ret} (subkey size is probably out of range)")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
subkey
|
subkey
|
||||||
end
|
end
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
require "./lib_sodium"
|
require "./lib_sodium"
|
||||||
require "./wipe"
|
require "./wipe"
|
||||||
|
require "crypto-secret/stateful"
|
||||||
|
|
||||||
module Sodium
|
module Sodium
|
||||||
# Allocate guarded memory using [sodium_malloc](https://libsodium.gitbook.io/doc/memory_management)
|
# Allocate guarded memory using [sodium_malloc](https://libsodium.gitbook.io/doc/memory_management)
|
||||||
@ -7,44 +8,17 @@ module Sodium
|
|||||||
# #initialize returns readonly or readwrite for thread safety
|
# #initialize returns readonly or readwrite for thread safety
|
||||||
# When state changes are required (such as using #noaccess) and the buffer is accessed from multiple threads wrap each #readonly/#readwrite block in a lock.
|
# When state changes are required (such as using #noaccess) and the buffer is accessed from multiple threads wrap each #readonly/#readwrite block in a lock.
|
||||||
class SecureBuffer
|
class SecureBuffer
|
||||||
class Error < Sodium::Error
|
include Crypto::Secret::Stateful
|
||||||
class KeyWiped < Error
|
|
||||||
end
|
|
||||||
|
|
||||||
class InvalidStateTransition < Error
|
# @state = State::Readwrite
|
||||||
end
|
|
||||||
|
|
||||||
# Check RLIMIT_MEMLOCK if you receive this
|
getter bytesize : Int32
|
||||||
class OutOfMemory < Error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
enum State
|
|
||||||
Cloning
|
|
||||||
Wiped
|
|
||||||
Noaccess
|
|
||||||
Readonly
|
|
||||||
Readwrite
|
|
||||||
end
|
|
||||||
|
|
||||||
@state = State::Readwrite
|
|
||||||
|
|
||||||
getter bytesize
|
|
||||||
|
|
||||||
delegate :+, :[], :[]=, :hexstring, to: to_slice
|
|
||||||
|
|
||||||
def initialize(@bytesize : Int32)
|
def initialize(@bytesize : Int32)
|
||||||
@ptr = LibSodium.sodium_malloc @bytesize
|
@ptr = LibSodium.sodium_malloc @bytesize
|
||||||
raise Error::OutOfMemory.new if @ptr.null?
|
raise Error::OutOfMemory.new if @ptr.null?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns a **readonly** random SecureBuffer.
|
|
||||||
def self.random(size)
|
|
||||||
buf = new(size)
|
|
||||||
Random::Secure.random_bytes buf.to_slice
|
|
||||||
buf.readonly
|
|
||||||
end
|
|
||||||
|
|
||||||
# Copies bytes to a **readonly** SecureBuffer.
|
# Copies bytes to a **readonly** SecureBuffer.
|
||||||
# Optionally erases bytes after copying if erase is set
|
# Optionally erases bytes after copying if erase is set
|
||||||
# Returns a **readonly** SecureBuffer.
|
# Returns a **readonly** SecureBuffer.
|
||||||
@ -61,30 +35,16 @@ module Sodium
|
|||||||
initialize sbuf.bytesize
|
initialize sbuf.bytesize
|
||||||
|
|
||||||
# Maybe not thread safe
|
# Maybe not thread safe
|
||||||
sbuf.readonly do
|
sbuf.readonly do |s1|
|
||||||
sbuf.to_slice.copy_to self.to_slice
|
self.to_slice do |s2|
|
||||||
|
s1.copy_to s2.to_slice
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@state = State::Cloning
|
@state = State::Cloning
|
||||||
set_state sbuf.@state
|
set_state sbuf.@state
|
||||||
end
|
end
|
||||||
|
|
||||||
# WARNING: Not thread safe
|
|
||||||
def wipe
|
|
||||||
return if @state == State::Wiped
|
|
||||||
readwrite
|
|
||||||
Sodium.memzero self.to_slice
|
|
||||||
@state = State::Wiped
|
|
||||||
noaccess!
|
|
||||||
end
|
|
||||||
|
|
||||||
# WARNING: Not thread safe
|
|
||||||
def wipe
|
|
||||||
yield
|
|
||||||
ensure
|
|
||||||
wipe
|
|
||||||
end
|
|
||||||
|
|
||||||
# :nodoc:
|
# :nodoc:
|
||||||
def finalize
|
def finalize
|
||||||
LibSodium.sodium_free @ptr
|
LibSodium.sodium_free @ptr
|
||||||
@ -103,6 +63,11 @@ module Sodium
|
|||||||
Slice(UInt8).new @ptr, @bytesize
|
Slice(UInt8).new @ptr, @bytesize
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_slice(& : Bytes -> Nil)
|
||||||
|
yield Bytes.new @ptr, @bytesize
|
||||||
|
end
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
def to_unsafe
|
def to_unsafe
|
||||||
@ptr
|
@ptr
|
||||||
end
|
end
|
||||||
@ -112,98 +77,21 @@ module Sodium
|
|||||||
self.class.new self
|
self.class.new self
|
||||||
end
|
end
|
||||||
|
|
||||||
# Temporarily make buffer readonly within the block returning to the prior state on exit.
|
protected def readwrite_impl : Nil
|
||||||
# WARNING: Not thread safe unless this object is readonly or readwrite
|
|
||||||
def readonly
|
|
||||||
with_state State::Readonly do
|
|
||||||
yield
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Temporarily make buffer readwrite within the block returning to the prior state on exit.
|
|
||||||
# WARNING: Not thread safe unless this object is **readwrite**
|
|
||||||
def readwrite
|
|
||||||
with_state State::Readwrite do
|
|
||||||
yield
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Makes a region allocated using sodium_malloc() or sodium_allocarray() inaccessible. It cannot be read or written, but the data are preserved.
|
|
||||||
# WARNING: Not thread safe
|
|
||||||
def noaccess
|
|
||||||
raise Error::KeyWiped.new if @state == State::Wiped
|
|
||||||
noaccess!
|
|
||||||
@state = State::Noaccess
|
|
||||||
self
|
|
||||||
end
|
|
||||||
|
|
||||||
# Also used by #wipe
|
|
||||||
private def noaccess!
|
|
||||||
if LibSodium.sodium_mprotect_noaccess(@ptr) != 0
|
|
||||||
raise "sodium_mprotect_noaccess"
|
|
||||||
end
|
|
||||||
self
|
|
||||||
end
|
|
||||||
|
|
||||||
# Marks a region allocated using sodium_malloc() or sodium_allocarray() as read-only.
|
|
||||||
# WARNING: Not thread safe
|
|
||||||
def readonly
|
|
||||||
raise Error::KeyWiped.new if @state == State::Wiped
|
|
||||||
if LibSodium.sodium_mprotect_readonly(@ptr) != 0
|
|
||||||
raise "sodium_mprotect_readonly"
|
|
||||||
end
|
|
||||||
@state = State::Readonly
|
|
||||||
self
|
|
||||||
end
|
|
||||||
|
|
||||||
# Marks a region allocated using sodium_malloc() or sodium_allocarray() as readable and writable, after having been protected using sodium_mprotect_readonly() or sodium_mprotect_noaccess().
|
|
||||||
# WARNING: Not thread safe
|
|
||||||
def readwrite
|
|
||||||
raise Error::KeyWiped.new if @state == State::Wiped
|
|
||||||
if LibSodium.sodium_mprotect_readwrite(@ptr) != 0
|
if LibSodium.sodium_mprotect_readwrite(@ptr) != 0
|
||||||
raise "sodium_mprotect_readwrite"
|
raise "sodium_mprotect_readwrite"
|
||||||
end
|
end
|
||||||
@state = State::Readwrite
|
|
||||||
self
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Timing safe memory compare.
|
protected def readonly_impl : Nil
|
||||||
def ==(other : self)
|
if LibSodium.sodium_mprotect_readonly(@ptr) != 0
|
||||||
Sodium.memcmp self.to_slice, other.to_slice
|
raise "sodium_mprotect_readonly"
|
||||||
end
|
|
||||||
|
|
||||||
# Timing safe memory compare.
|
|
||||||
def ==(other : Bytes)
|
|
||||||
Sodium.memcmp self.to_slice, other
|
|
||||||
end
|
|
||||||
|
|
||||||
# WARNING: Not thread safe
|
|
||||||
private def set_state(new_state : State)
|
|
||||||
return if @state == new_state
|
|
||||||
|
|
||||||
case new_state
|
|
||||||
when State::Readwrite; readwrite
|
|
||||||
when State::Readonly ; readonly
|
|
||||||
when State::Noaccess ; noaccess
|
|
||||||
when State::Wiped ; raise Error::InvalidStateTransition.new
|
|
||||||
else
|
|
||||||
raise "unknown state #{new_state}"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# WARNING: Only thread safe when current state >= requested state
|
protected def noaccess_impl : Nil
|
||||||
private def with_state(new_state : State)
|
if LibSodium.sodium_mprotect_noaccess(@ptr) != 0
|
||||||
old_state = @state
|
raise "sodium_mprotect_noaccess"
|
||||||
# Only change when new_state needs more access than @state.
|
|
||||||
if old_state >= new_state
|
|
||||||
yield
|
|
||||||
else
|
|
||||||
begin
|
|
||||||
set_state new_state
|
|
||||||
yield
|
|
||||||
ensure
|
|
||||||
set_state old_state
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
Loading…
Reference in New Issue
Block a user