summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoricebaker <113217272+icebaker@users.noreply.github.com>2023-06-03 20:37:14 -0300
committerGitHub <noreply@github.com>2023-06-03 20:37:14 -0300
commitbffbac01eeb00e5f94cd4d675edc0a0566354265 (patch)
tree39df8578dd2bbed230e0d222d0175e088f97537e
parent2c50a06b68a21ce904e5dfd15833e3569ff64bfa (diff)
parent1be75c768ca9595b54d8e2d5a8287adbc950f659 (diff)
Merge pull request #3 from icebaker/ib-cryptography
Cryptography and Security
-rw-r--r--Gemfile.lock7
-rw-r--r--README.md95
-rw-r--r--components/crypto.rb43
-rw-r--r--components/provider.rb4
-rw-r--r--components/providers/openai.rb19
-rw-r--r--components/storage.rb33
-rw-r--r--controllers/cartridges.rb2
-rw-r--r--controllers/instance.rb8
-rw-r--r--controllers/interfaces/cli.rb23
-rw-r--r--controllers/interfaces/eval.rb4
-rw-r--r--controllers/security.rb27
-rw-r--r--controllers/session.rb12
-rw-r--r--docker-compose.example.yml1
-rw-r--r--nano-bots.gemspec1
-rw-r--r--ports/dsl/nano-bots.rb20
15 files changed, 269 insertions, 30 deletions
diff --git a/Gemfile.lock b/Gemfile.lock
index facc989..f5acf72 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -7,6 +7,7 @@ PATH
faraday (~> 2.7, >= 2.7.5)
pry (~> 0.14.2)
rainbow (~> 3.1, >= 3.1.1)
+ rbnacl (~> 7.1, >= 7.1.1)
ruby-openai (~> 4.0)
sweet-moon (~> 0.0.7)
@@ -35,6 +36,8 @@ GEM
coderay (~> 1.1)
method_source (~> 1.0)
rainbow (3.1.1)
+ rbnacl (7.1.1)
+ ffi
regexp_parser (2.8.0)
rexml (3.2.5)
rspec (3.12.0)
@@ -50,7 +53,7 @@ GEM
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-support (3.12.0)
- rubocop (1.51.0)
+ rubocop (1.52.0)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.2.0.0)
@@ -60,7 +63,7 @@ GEM
rubocop-ast (>= 1.28.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
- rubocop-ast (1.28.1)
+ rubocop-ast (1.29.0)
parser (>= 3.2.1.0)
rubocop-capybara (2.18.0)
rubocop (~> 1.41)
diff --git a/README.md b/README.md
index 9a87c97..622de2f 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,10 @@ https://user-images.githubusercontent.com/113217272/238141567-c58a240c-7b67-4b3b
- [Library](#library)
- [Cartridges](#cartridges)
- [Marketplace](#marketplace)
+- [Security and Privacy](#security-and-privacy)
+ - [Cryptography](#cryptography)
+ - [End-user IDs](#end-user-ids)
+ - [Decrypting](#decrypting)
- [Providers](#providers)
- [Debugging](#debugging)
- [Development](#development)
@@ -40,6 +44,7 @@ bundle install
For credentials and configurations, relevant environment variables can be set in your `.bashrc`, `.zshrc`, or equivalent files, as well as in your Docker Container or System Environment. Example:
```sh
+export NANO_BOTS_ENCRYPTION_PASSWORD="UNSAFE"
export OPENAI_API_ADDRESS=https://api.openai.com
export OPENAI_API_ACCESS_TOKEN=your-token
export OPENAI_API_USER_IDENTIFIER=your-user
@@ -51,6 +56,7 @@ export OPENAI_API_USER_IDENTIFIER=your-user
Alternatively, if your current directory has a `.env` file with the environment variables, they will be automatically loaded:
```sh
+NANO_BOTS_ENCRYPTION_PASSWORD="UNSAFE"
OPENAI_API_ADDRESS=https://api.openai.com
OPENAI_API_ACCESS_TOKEN=your-token
OPENAI_API_USER_IDENTIFIER=your-user
@@ -79,6 +85,7 @@ services:
image: ruby:3.2.2-slim-bullseye
command: sh -c "gem install nano-bots -v 0.0.9 && bash"
environment:
+ NANO_BOTS_ENCRYPTION_PASSWORD: UNSAFE
OPENAI_API_ADDRESS: https://api.openai.com
OPENAI_API_ACCESS_TOKEN: your-token
OPENAI_API_USER_IDENTIFIER: your-user
@@ -266,6 +273,94 @@ Try the [Nano Bots Clinic (Live Editor)](https://clinic.nbots.io) to learn about
You can explore the Nano Bots [Marketplace](https://nbots.io) to discover new Cartridges that can help you.
+## Security and Privacy
+
+Each provider will have its own security and privacy policies (e.g. [OpenAI Policy](https://openai.com/policies/api-data-usage-policies)), so you must consult them to understand their implications.
+
+### Cryptography
+
+By default, all states stored in your local disk are encrypted.
+
+To ensure that the encryption is secure, you need to define a password through the `NANO_BOTS_ENCRYPTION_PASSWORD` environment variable. Otherwise, although the content will be encrypted, anyone would be able to decrypt it without a password.
+
+It's important to note that the content shared with providers, despite being transmitted over secure connections (e.g., [HTTPS](https://en.wikipedia.org/wiki/HTTPS)), will be readable by the provider. This is because providers need to operate on the data, which would not be possible if the content was encrypted beyond HTTPS. So, the data stored locally on your system is encrypted, which does not mean that what you share with providers will not be readable by them.
+
+To ensure that your encryption and password are configured properly, you can run the following command:
+```sh
+nb security
+```
+
+Which should return:
+```text
+āœ… Encryption is enabled and properly working.
+ This means that your data is stored in an encrypted format on your disk.
+
+āœ… A password is being used for the encrypted content.
+ This means that only those who possess the password can decrypt your data.
+```
+
+Alternatively, you can check it at runtime with:
+```ruby
+require 'nano-bots'
+
+NanoBot.security.check
+# => { encryption: true, password: true }
+```
+
+#### End-user IDs
+
+A common strategy for deploying Nano Bots to multiple users through APIs or automations is to assign a unique [end-user ID](https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids) for each user. This can be useful if any of your users violate the provider's policy due to abusive behavior. By providing the end-user ID, you can unravel that even though the activity originated from your API Key, the actions taken were not your own.
+
+You can define custom end-user identifiers in the following way:
+
+```ruby
+NanoBot.new(environment: { NANO_BOTS_USER_IDENTIFIER: 'user-a' })
+NanoBot.new(environment: { NANO_BOTS_USER_IDENTIFIER: 'user-b' })
+```
+
+Consider that you have have the following OpenAI user identifier:
+```sh
+OPENAI_API_USER_IDENTIFIER=your-name
+```
+
+The requests will be performed as follows:
+
+```ruby
+NanoBot.new(environment: {NANO_BOTS_USER_IDENTIFIER: 'user-a'})
+# { user: 'your-name/user-a' }
+
+NanoBot.new(environment: {NANO_BOTS_USER_IDENTIFIER: 'user-b'})
+# { user: 'your-name/user-b' }
+```
+
+Actually, to enhance privacy, neither your user nor your users' identifiers will be shared in this way. Instead, they will be encrypted before being shared with the provider:
+
+```ruby
+'your-name/user-a'
+# -onBK9GWafYz-JM8-cydhn4jd4Bkfkec5FtJ1ReCrtHCDPjkhCqUjRobG1zLnAz3BLo1kFhRW3w=
+
+'your-name/user-a'
+# RldW4_xxktCksEAR9G8aORuq3skPAc9ivWj3eye2ICCHQy8gG_R5qLMS3Fg-0lY6LwxKGQur5Ww=
+```
+
+In this manner, you possess identifiers if required, however, their actual content can only be decrypted by you via your secure password (`NANO_BOTS_ENCRYPTION_PASSWORD`).
+
+## Decrypting
+
+To decrypt your encrypted data, once you have properly configured your password, you can simply run:
+
+```ruby
+require 'nano-bots'
+
+NanoBot.security.decrypt('-onBK9GWafYz-JM8-cydhn4jd4Bkfkec5FtJ1ReCrtHCDPjkhCqUjRobG1zLnAz3BLo1kFhRW3w=')
+# your-name/user-b
+
+NanoBot.security.decrypt('RldW4_xxktCksEAR9G8aORuq3skPAc9ivWj3eye2ICCHQy8gG_R5qLMS3Fg-0lY6LwxKGQur5Ww=')
+# your-name/user-b
+```
+
+If you lose your password, you lose your data. It is not possible to recover it at all. For real.
+
## Providers
Currently supported providers:
diff --git a/components/crypto.rb b/components/crypto.rb
new file mode 100644
index 0000000..1848c9b
--- /dev/null
+++ b/components/crypto.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'singleton'
+require 'rbnacl'
+require 'base64'
+
+module NanoBot
+ module Components
+ class Crypto
+ include Singleton
+
+ def initialize
+ password = ENV.fetch('NANO_BOTS_ENCRYPTION_PASSWORD', nil)
+
+ password = 'UNSAFE' unless password && password != ''
+
+ @box = RbNaCl::SecretBox.new(RbNaCl::Hash.sha256(password))
+ @fixed_nonce = RbNaCl::Hash.sha256(password)[0...@box.nonce_bytes]
+ end
+
+ def encrypt(content, soft: false)
+ nonce = soft ? @fixed_nonce : RbNaCl::Random.random_bytes(@box.nonce_bytes)
+ Base64.urlsafe_encode64(nonce + @box.encrypt(nonce, content))
+ end
+
+ def decrypt(content)
+ decoded_content = Base64.urlsafe_decode64(content)
+ nonce = decoded_content[0...@box.nonce_bytes]
+ cipher_text = decoded_content[@box.nonce_bytes..]
+
+ @box.decrypt(nonce, cipher_text)
+ end
+
+ def self.encrypt(content, soft: false)
+ instance.encrypt(content, soft:)
+ end
+
+ def self.decrypt(content)
+ instance.decrypt(content)
+ end
+ end
+ end
+end
diff --git a/components/provider.rb b/components/provider.rb
index dbfc8bd..3138cc4 100644
--- a/components/provider.rb
+++ b/components/provider.rb
@@ -7,10 +7,10 @@ require_relative './providers/openai'
module NanoBot
module Components
class Provider
- def self.new(provider)
+ def self.new(provider, environment: {})
case provider[:name]
when 'openai'
- Providers::OpenAI.new(provider[:settings])
+ Providers::OpenAI.new(provider[:settings], environment:)
else
raise "Unsupported provider #{provider[:name]}"
end
diff --git a/components/providers/openai.rb b/components/providers/openai.rb
index c0a6639..c64a588 100644
--- a/components/providers/openai.rb
+++ b/components/providers/openai.rb
@@ -3,6 +3,7 @@
require 'openai'
require_relative './base'
+require_relative '../crypto'
module NanoBot
module Components
@@ -15,8 +16,9 @@ module NanoBot
attr_reader :settings
- def initialize(settings)
+ def initialize(settings, environment: {})
@settings = settings
+ @environment = environment
@client = ::OpenAI::Client.new(
uri_base: "#{@settings[:credentials][:address].sub(%r{/$}, '')}/",
@@ -46,11 +48,16 @@ module NanoBot
)
end
- payload = {
- model: @settings[:model],
- user: @settings[:credentials][:'user-identifier'],
- messages:
- }
+ user = @settings[:credentials][:'user-identifier']
+
+ user_suffix = @environment && (
+ @environment['NANO_BOTS_USER_IDENTIFIER'] ||
+ @environment[:NANO_BOTS_USER_IDENTIFIER]
+ )
+
+ user = "#{user}/#{user_suffix}" if user_suffix && user_suffix != ''
+
+ payload = { model: @settings[:model], user: Crypto.encrypt(user, soft: true), messages: }
CHAT_SETTINGS.each do |key|
payload[key] = @settings[key] if @settings.key?(key)
diff --git a/components/storage.rb b/components/storage.rb
index cba3dd0..b6d8910 100644
--- a/components/storage.rb
+++ b/components/storage.rb
@@ -3,11 +3,12 @@
require 'babosa'
require_relative '../logic/helpers/hash'
+require_relative './crypto'
module NanoBot
module Components
class Storage
- def self.build_path_and_ensure_state_file!(key, cartridge)
+ def self.build_path_and_ensure_state_file!(key, cartridge, environment: {})
path = [
Logic::Helpers::Hash.fetch(cartridge, %i[state directory]),
ENV.fetch('NANO_BOTS_STATE_DIRECTORY', nil)
@@ -17,14 +18,38 @@ module NanoBot
path = "#{user_home!.sub(%r{/$}, '')}/.local/state/nano-bots" if path.nil?
- path = "#{path.sub(%r{/$}, '')}/ruby-nano-bots/#{cartridge[:meta][:author].to_slug.normalize}"
+ prefix = environment && (
+ environment['NANO_BOTS_USER_IDENTIFIER'] ||
+ environment[:NANO_BOTS_USER_IDENTIFIER]
+ )
+
+ path = "#{path.sub(%r{/$}, '')}/ruby-nano-bots/vault"
+
+ if prefix
+ normalized = prefix.split('/').map do |part|
+ Crypto.encrypt(
+ part.to_s.gsub('.', '-').force_encoding('UTF-8').to_slug.normalize,
+ soft: true
+ )
+ end.join('/')
+
+ path = "#{path}/#{normalized}"
+ end
+
+ path = "#{path}/#{cartridge[:meta][:author].to_slug.normalize}"
path = "#{path}/#{cartridge[:meta][:name].to_slug.normalize}"
- path = "#{path}/#{cartridge[:meta][:version].to_s.gsub('.', '-').to_slug.normalize}/#{key}"
+ path = "#{path}/#{cartridge[:meta][:version].to_s.gsub('.', '-').to_slug.normalize}"
+ path = "#{path}/#{Crypto.encrypt(key, soft: true)}"
path = "#{path}/state.json"
FileUtils.mkdir_p(File.dirname(path))
- File.write(path, JSON.generate({ key:, history: [] })) unless File.exist?(path)
+ unless File.exist?(path)
+ File.write(
+ path,
+ Crypto.encrypt(JSON.generate({ key:, history: [] }))
+ )
+ end
path
end
diff --git a/controllers/cartridges.rb b/controllers/cartridges.rb
index 151cc46..fe0d56e 100644
--- a/controllers/cartridges.rb
+++ b/controllers/cartridges.rb
@@ -39,7 +39,7 @@ module NanoBot
rescue StandardError => _e
end
- cartridges.sort_by { |cartridge| cartridge[:meta][:name] }
+ cartridges = cartridges.sort_by { |cartridge| cartridge[:meta][:name] }
cartridges.prepend(
{ system: { id: '-' }, meta: { name: 'Default', symbol: 'šŸ¤–' } }
diff --git a/controllers/instance.rb b/controllers/instance.rb
index a982261..d4e0c1b 100644
--- a/controllers/instance.rb
+++ b/controllers/instance.rb
@@ -13,14 +13,16 @@ require_relative './session'
module NanoBot
module Controllers
class Instance
- def initialize(cartridge_path:, stream:, state: nil)
+ def initialize(cartridge_path:, stream:, state: nil, environment: {})
@stream = stream
load_cartridge!(cartridge_path)
- provider = Components::Provider.new(@cartridge[:provider])
+ provider = Components::Provider.new(@cartridge[:provider], environment:)
- @session = Session.new(provider:, cartridge: @cartridge, state:, stream: @stream)
+ @session = Session.new(
+ provider:, cartridge: @cartridge, state:, stream: @stream, environment:
+ )
end
def cartridge
diff --git a/controllers/interfaces/cli.rb b/controllers/interfaces/cli.rb
index da027f7..ae066cd 100644
--- a/controllers/interfaces/cli.rb
+++ b/controllers/interfaces/cli.rb
@@ -12,6 +12,28 @@ module NanoBot
when 'version'
puts NanoBot::GEM[:version]
exit
+ when 'security'
+ result = NanoBot.security.check
+
+ if result[:encryption]
+ puts "\nāœ… Encryption is enabled and properly working."
+ puts ' This means that your data is stored in an encrypted format on your disk.'
+ else
+ puts "\nāŒ Encryption is not being utilized to store your content."
+ puts ' This means that your data can be easily read because it is stored in plaintext.'
+ end
+
+ if result[:password]
+ puts "\nāœ… A password is being used for the encrypted content."
+ puts ' This means that only those who possess the password can decrypt your data.'
+ else
+ puts "\nāŒ No custom password is being used for the encrypted content."
+ puts ' This means that anyone can easily decrypt your data.'
+ end
+
+ puts ''
+
+ exit
when 'help', '', nil
puts ''
puts "Nano Bots #{NanoBot::GEM[:version]}"
@@ -34,6 +56,7 @@ module NanoBot
puts ' nb - STATE-KEY state'
puts ' nb cartridge.yml STATE-KEY state'
puts ''
+ puts ' nb security'
puts ' nb version'
puts ' nb help'
puts ''
diff --git a/controllers/interfaces/eval.rb b/controllers/interfaces/eval.rb
index 5f472ad..9613b28 100644
--- a/controllers/interfaces/eval.rb
+++ b/controllers/interfaces/eval.rb
@@ -1,9 +1,5 @@
# frozen_string_literal: true
-require 'pry'
-require 'rainbow'
-
-require_relative '../../logic/helpers/hash'
require_relative '../../logic/cartridge/affixes'
module NanoBot
diff --git a/controllers/security.rb b/controllers/security.rb
new file mode 100644
index 0000000..8f066a5
--- /dev/null
+++ b/controllers/security.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require_relative '../components/crypto'
+
+module NanoBot
+ module Controllers
+ module Security
+ def self.decrypt(content)
+ Components::Crypto.decrypt(content)
+ end
+
+ def self.check
+ password = ENV.fetch('NANO_BOTS_ENCRYPTION_PASSWORD', nil)
+ password = 'UNSAFE' unless password && password != ''
+
+ {
+ encryption: (
+ Components::Crypto.encrypt('SAFE') != 'SAFE' &&
+ Components::Crypto.encrypt('SAFE') != Components::Crypto.encrypt('SAFE') &&
+ Components::Crypto.decrypt(Components::Crypto.encrypt('SAFE')) == 'SAFE'
+ ),
+ password: password != 'UNSAFE'
+ }
+ end
+ end
+ end
+end
diff --git a/controllers/session.rb b/controllers/session.rb
index 270b623..4694911 100644
--- a/controllers/session.rb
+++ b/controllers/session.rb
@@ -9,6 +9,7 @@ require_relative '../logic/cartridge/streaming'
require_relative '../logic/cartridge/interaction'
require_relative '../components/storage'
require_relative '../components/adapter'
+require_relative '../components/crypto'
module NanoBot
module Controllers
@@ -17,7 +18,7 @@ module NanoBot
class Session
attr_accessor :stream
- def initialize(provider:, cartridge:, state: nil, stream: $stdout)
+ def initialize(provider:, cartridge:, state: nil, stream: $stdout, environment: {})
@stream = stream
@provider = provider
@cartridge = cartridge
@@ -28,8 +29,9 @@ module NanoBot
@state = { history: [] }
else
@state_path = Components::Storage.build_path_and_ensure_state_file!(
- state.strip, @cartridge
+ state.strip, @cartridge, environment:
)
+
@state = load_state
end
end
@@ -39,11 +41,13 @@ module NanoBot
end
def load_state
- @state = Logic::Helpers::Hash.symbolize_keys(JSON.parse(File.read(@state_path)))
+ @state = Logic::Helpers::Hash.symbolize_keys(JSON.parse(
+ Components::Crypto.decrypt(File.read(@state_path))
+ ))
end
def store_state!
- File.write(@state_path, JSON.generate(@state))
+ File.write(@state_path, Components::Crypto.encrypt(JSON.generate(@state)))
end
def boot(mode:)
diff --git a/docker-compose.example.yml b/docker-compose.example.yml
index 2c0249c..d56e495 100644
--- a/docker-compose.example.yml
+++ b/docker-compose.example.yml
@@ -5,6 +5,7 @@ services:
image: ruby:3.2.2-slim-bullseye
command: sh -c "apt-get update && apt-get install -y --no-install-recommends build-essential libffi-dev lua5.4-dev && gem install nano-bots -v 0.0.9 && bash"
environment:
+ NANO_BOTS_ENCRYPTION_PASSWORD: UNSAFE
OPENAI_API_ADDRESS: https://api.openai.com
OPENAI_API_ACCESS_TOKEN: your-token
OPENAI_API_USER_IDENTIFIER: your-user
diff --git a/nano-bots.gemspec b/nano-bots.gemspec
index eb6f748..d66ec6c 100644
--- a/nano-bots.gemspec
+++ b/nano-bots.gemspec
@@ -36,6 +36,7 @@ Gem::Specification.new do |spec|
spec.add_dependency 'faraday', '~> 2.7', '>= 2.7.5'
spec.add_dependency 'pry', '~> 0.14.2'
spec.add_dependency 'rainbow', '~> 3.1', '>= 3.1.1'
+ spec.add_dependency 'rbnacl', '~> 7.1', '>= 7.1.1'
spec.add_dependency 'ruby-openai', '~> 4.0'
spec.add_dependency 'sweet-moon', '~> 0.0.7'
diff --git a/ports/dsl/nano-bots.rb b/ports/dsl/nano-bots.rb
index 89da466..29cc828 100644
--- a/ports/dsl/nano-bots.rb
+++ b/ports/dsl/nano-bots.rb
@@ -5,12 +5,22 @@ require 'dotenv/load'
require_relative '../../static/gem'
require_relative '../../controllers/cartridges'
require_relative '../../controllers/instance'
+require_relative '../../controllers/security'
require_relative '../../controllers/interfaces/cli'
require_relative '../../components/stream'
module NanoBot
- def self.new(cartridge: '-', state: '-')
- Controllers::Instance.new(cartridge_path: cartridge, state:, stream: Components::Stream.new)
+ def self.new(cartridge: '-', state: '-', environment: {})
+ Controllers::Instance.new(
+ cartridge_path: cartridge,
+ state:,
+ stream: Components::Stream.new,
+ environment:
+ )
+ end
+
+ def self.security
+ Controllers::Security
end
def self.cartridges
@@ -21,8 +31,10 @@ module NanoBot
Controllers::Interfaces::CLI.handle!
end
- def self.repl(cartridge: '-', state: '-')
- Controllers::Instance.new(cartridge_path: cartridge, state:, stream: $stdout).repl
+ def self.repl(cartridge: '-', state: '-', environment: {})
+ Controllers::Instance.new(
+ cartridge_path: cartridge, state:, stream: $stdout, environment:
+ ).repl
end
def self.version