Sodium::SecureBuffer is now a Crypto::Secret
This commit is contained in:
		
							parent
							
								
									cd0ce1ccdb
								
							
						
					
					
						commit
						fca40d7764
					
				
					 4 changed files with 56 additions and 151 deletions
				
			
		|  | @ -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,8 +7,10 @@ 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| | ||||||
|       b.should eq 0xdb_u8 |       slice.each do |b| | ||||||
|  |         b.should eq 0xdb_u8 | ||||||
|  |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     buf.noaccess |     buf.noaccess | ||||||
|  | @ -17,18 +19,22 @@ 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 | ||||||
|       b.should eq 1_u8 | 
 | ||||||
|  |       slice.each do |b| | ||||||
|  |         b.should eq 1_u8 | ||||||
|  |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     bytes.to_slice.each do |b| |     bytes.to_slice.each do |b| | ||||||
|  | @ -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,9 +79,11 @@ module Sodium | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       subkey = SecureBuffer.new subkey_size |       subkey = SecureBuffer.new subkey_size | ||||||
|       @sbuf.readonly do |       subkey.readwrite do |sub_slice| | ||||||
|         if (ret = LibSodium.crypto_kdf_derive_from_key(subkey, subkey.bytesize, subkey_id, context, self.to_slice)) != 0 |         @sbuf.readonly do | ||||||
|           raise Sodium::Error.new("crypto_kdf_derive_from_key returned #{ret} (subkey size is probably out of range)") |           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)") | ||||||
|  |           end | ||||||
|         end |         end | ||||||
|       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…
	
	Add table
		
		Reference in a new issue
	
	 Didactic Drunk
						Didactic Drunk