{"id":13879767,"url":"https://github.com/fnando/attr_keyring","last_synced_at":"2025-04-12T19:41:40.368Z","repository":{"id":33613878,"uuid":"160004670","full_name":"fnando/attr_keyring","owner":"fnando","description":"Simple encryption-at-rest with key rotation support for Ruby.","archived":false,"fork":false,"pushed_at":"2024-04-24T20:44:09.000Z","size":376,"stargazers_count":18,"open_issues_count":2,"forks_count":2,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-04-24T21:39:48.361Z","etag":null,"topics":["activerecord","encryption","key-rotation","keyring","rotation-encryption","sequel"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fnando.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":["fnando"],"custom":["https://www.paypal.me/nandovieira/🍕"]}},"created_at":"2018-12-02T01:56:37.000Z","updated_at":"2024-06-02T21:27:16.010Z","dependencies_parsed_at":"2024-06-02T21:27:08.363Z","dependency_job_id":"791ea9de-ccf4-441e-9f12-f3828b348415","html_url":"https://github.com/fnando/attr_keyring","commit_stats":{"total_commits":63,"total_committers":3,"mean_commits":21.0,"dds":"0.11111111111111116","last_synced_commit":"16d6e9ae6bc7cc46d2150d809d552ac7523fefc1"},"previous_names":[],"tags_count":16,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fnando%2Fattr_keyring","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fnando%2Fattr_keyring/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fnando%2Fattr_keyring/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fnando%2Fattr_keyring/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fnando","download_url":"https://codeload.github.com/fnando/attr_keyring/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248625033,"owners_count":21135509,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["activerecord","encryption","key-rotation","keyring","rotation-encryption","sequel"],"created_at":"2024-08-06T08:02:32.227Z","updated_at":"2025-04-12T19:41:40.341Z","avatar_url":"https://github.com/fnando.png","language":"Ruby","funding_links":["https://github.com/sponsors/fnando","https://www.paypal.me/nandovieira/🍕"],"categories":["Ruby"],"sub_categories":[],"readme":"![attr_keyring: Simple encryption-at-rest with key rotation support for Ruby.](https://raw.githubusercontent.com/fnando/attr_keyring/main/attr_keyring.png)\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/fnando/attr_keyring/actions?query=workflow%3ATests\"\u003e\u003cimg src=\"https://github.com/fnando/attr_keyring/workflows/Tests/badge.svg\" alt=\"Tests\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://codeclimate.com/github/fnando/attr_keyring\"\u003e\u003cimg src=\"https://codeclimate.com/github/fnando/attr_keyring/badges/gpa.svg\" alt=\"Code Climate\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://rubygems.org/gems/attr_keyring\"\u003e\u003cimg src=\"https://img.shields.io/gem/v/attr_keyring.svg\" alt=\"Gem\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://rubygems.org/gems/attr_keyring\"\u003e\u003cimg src=\"https://img.shields.io/gem/dt/attr_keyring.svg\" alt=\"Gem\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\nN.B.: attr_keyring is not for encrypting passwords--for that, you should use\nsomething like [bcrypt](https://github.com/codahale/bcrypt-ruby). It's meant for\nencrypting sensitive data you will need to access in plain text (e.g. storing\nOAuth token from users). Passwords do not fall in that category.\n\nThis library is heavily inspired by\n[attr_vault](https://github.com/uhoh-itsmaciek/attr_vault), and can read\nencrypted messages if you encode them in base64 (e.g.\n`Base64.strict_encode64(encrypted_by_attr_vault)`).\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem \"attr_keyring\"\n```\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install attr_keyring\n\n## Usage\n\n### Basic usage\n\n```ruby\ngem \"attr_keyring\"\nrequire \"keyring\"\n\nkeyring = Keyring.new(\n  {\"1\" =\u003e \"uDiMcWVNTuz//naQ88sOcN+E40CyBRGzGTT7OkoBS6M=\"},\n  digest_salt: \"\u003ccustom salt\u003e\"\n)\n\n# STEP 1: Encrypt message using latest encryption key.\nencrypted, keyring_id, digest = keyring.encrypt(\"super secret\")\n\nputs \"🔒 #{encrypted}\"\nputs \"🔑 #{keyring_id}\"\nputs \"🔎 #{digest}\"\n\n# STEP 2: Decrypted message using encryption key defined by keyring id.\ndecrypted = keyring.decrypt(encrypted, keyring_id)\nputs \"✉️ #{decrypted}\"\n```\n\n#### Change encryption algorithm\n\nYou can choose between `AES-128-CBC`, `AES-192-CBC` and `AES-256-CBC`. By\ndefault, `AES-128-CBC` will be used.\n\nTo specify the encryption algorithm, set the `encryption` option. The following\nexample uses `AES-256-CBC`.\n\n```ruby\nkeyring = Keyring.new(\n  \"1\" =\u003e \"uDiMcWVNTuz//naQ88sOcN+E40CyBRGzGTT7OkoBS6M=\",\n  encryptor: Keyring::Encryptor::AES::AES256CBC,\n  digest_salt: \"\u003ccustom salt\u003e\"\n)\n```\n\n### Configuration\n\nAs far as database schema goes:\n\n1. You'll need a column to track the key that was used for encryption; by\n   default it's called `keyring_id`.\n2. Every encrypted column must follow the name `encrypted_\u003ccolumn name\u003e`.\n3. Optionally, you can also have a `\u003ccolumn name\u003e_digest` to help with searching\n   (see Lookup section below).\n\nAs far as model configuration goes, they're pretty similar, as you can see\nbelow:\n\n#### ActiveRecord\n\nFrom Rails 5+, ActiveRecord models now inherit from `ApplicationRecord` instead.\nThis is how you set it up:\n\n```ruby\nclass ApplicationRecord \u003c ActiveRecord::Base\n  self.abstract_class = true\n  include AttrKeyring.active_record\nend\n```\n\n#### Sequel\n\nSequel doesn't have an abstract model class (but it could), so you can set up\nthe model class directly like the following:\n\n```ruby\nclass User \u003c Sequel::Model\n  include AttrKeyring.sequel\nend\n```\n\n### Defining encrypted attributes\n\nTo set up your model, you have to define the keyring (set of encryption keys)\nand the attributes that will be encrypted. Both ActiveRecord and Sequel have the\nsame API, so the examples below work for both ORMs.\n\n```ruby\nclass User \u003c ApplicationRecord\n  attr_keyring ENV[\"USER_KEYRING\"],\n               digest_salt: \"\u003ccustom salt\u003e\"\n  attr_encrypt :twitter_oauth_token, :social_security_number\nend\n```\n\nThe code above will encrypt your columns with the current key. If you're\nupdating a record, then the column will be migrated to the latest key available.\n\nYou can use the model as you would normally do.\n\n```ruby\nuser = User.create(\n  email: \"john@example.com\"\n)\n\nuser.email\n#=\u003e john@example.com\n\nuser.keyring_id\n#=\u003e 1\n\nuser.encrypted_email\n#=\u003e WG8Epo0ABz0Z1X5gX7kttc98w9Ei59B5uXGK36Zin9G0VqbxX3naOWOm4RI6w6Uu\n```\n\nIf you want to store a hash, you can use the `encoder:` option.\n\n```ruby\nclass User \u003c ApplicationRecord\n  attr_keyring ENV[\"USER_KEYRING\"],\n               digest_salt: \"\u003ccustom salt\u003e\"\n\n  attr_encrypt :data, encoder: JSON\nend\n```\n\nAn encoder is just an object that responds to the methods `dump(data)` and\n`parse(data)`, just like the `JSON` interface. Alternatively, you can use\n`AttrKeyring::Encoders::JSON`, which returns hashes with symbolized keys.\n\n### Encryption\n\nBy default, AES-128-CBC is the algorithm used for encryption. This algorithm\nuses 16 bytes keys, but you're required to use a key that's double the size\nbecause half of that keys will be used to generate the HMAC. The first 16 bytes\nwill be used as the encryption key, and the last 16 bytes will be used to\ngenerate the HMAC.\n\nUsing random data base64-encoded is the recommended way. You can easily generate\nkeys by using the following command:\n\n```console\n$ dd if=/dev/urandom bs=32 count=1 2\u003e/dev/null | openssl base64 -A\nqUjOJFgZsZbTICsN0TMkKqUvSgObYxnkHDsazTqE5tM=\n```\n\nInclude the result of this command in the `value` section of the key description\nin the keyring. Half this key is used for encryption, and half for the HMAC.\n\n#### Key size\n\nThe key size depends on the algorithm being used. The key size should be double\nthe size as half of it is used for HMAC computation.\n\n- `aes-128-cbc`: 16 bytes (encryption) + 16 bytes (HMAC).\n- `aes-192-cbc`: 24 bytes (encryption) + 24 bytes (HMAC).\n- `aes-256-cbc`: 32 bytes (encryption) + 32 bytes (HMAC).\n\n#### About the encrypted message\n\nInitialization vectors (IV) should be unpredictable and unique; ideally, they\nwill be cryptographically random. They do not have to be secret: IVs are\ntypically just added to ciphertext messages unencrypted. It may sound\ncontradictory that something has to be unpredictable and unique, but does not\nhave to be secret; it is important to remember that an attacker must not be able\nto predict ahead of time what a given IV will be.\n\nWith that in mind, _attr_keyring_ uses\n`base64(hmac(unencrypted iv + encrypted message) + unencrypted iv + encrypted message)`\nas the final message. If you're planning to migrate from other encryption\nmechanisms or read encrypted values from the database without using\n_attr_keyring_, make sure you account for this. The HMAC is 32-bytes long and\nthe IV is 16-bytes long.\n\n### Keyring\n\nKeys are managed through a keyring--a short JSON document describing your\nencryption keys. The keyring must be a JSON object mapping numeric ids of the\nkeys to the key values. A keyring must have at least one key. For example:\n\n```json\n{\n  \"1\": \"uDiMcWVNTuz//naQ88sOcN+E40CyBRGzGTT7OkoBS6M=\",\n  \"2\": \"VN8UXRVMNbIh9FWEFVde0q7GUA1SGOie1+FgAKlNYHc=\"\n}\n```\n\nThe `id` is used to track which key encrypted which piece of data; a key with a\nlarger id is assumed to be newer. The value is the actual bytes of the\nencryption key.\n\n#### Dynamically loading keyring\n\nIf you're using Rails 5.2+, you can use credentials to define your keyring. Your\n`credentials.yml` must be define like the following:\n\n```yaml\n---\nuser_keyring:\n  \"1\": \"QSXyoiRDPoJmfkJUZ4hJeQ==\"\n  \"2\": \"r6AfOeilPDJomFsiOXLdfQ==\"\n```\n\nThen you can setup your model by using\n`attr_keyring Rails.application.credentials.user_keyring`.\n\nOther possibilities (e.g. the keyring file is provided by configuration\nmanagement):\n\n- `attr_keyring YAML.load_file(keyring_file), digest_salt: \"\u003ccustom salt\u003e\"`\n- `attr_keyring JSON.parse(File.read(keyring_file)), digest_salt: \"\u003ccustom salt\u003e\"`.\n\n### Lookup\n\nOne tricky aspect of encryption is looking up records by known secret. E.g.,\n\n```ruby\nUser.where(email: \"john@example.com\")\n```\n\nis trivial with plain text fields, but impossible with the model defined as\nabove.\n\nIf a column `\u003cattribute\u003e_digest` exists, then a SHA1 digest from the value will\nbe saved. This will allow you to lookup by that value instead and add unique\nindexes. You don't have to use a hashing salt, but it's highly recommended; this\nway you can avoid leaking your users' info via rainbow tables.\n\n```ruby\nUser.where(email: User.keyring.digest(\"john@example.com\")).first\n```\n\n### Key Rotation\n\nBecause attr_keyring uses a keyring, with access to multiple keys at once, key\nrotation is fairly straightforward: if you add a key to the keyring with a\nhigher id than any other key, that key will automatically be used for encryption\nwhen records are either created or updated. Any keys that are no longer in use\ncan be safely removed from the keyring.\n\nTo check if an existing key with id `123` is still in use, run:\n\n```ruby\n# For a large dataset, you may want to index the `keyring_id` column.\nUser.where(keyring_id: 123).empty?\n```\n\nYou may not want to wait for records to be updated (e.g. key leaking). In that\ncase, you can rollout a key rotation:\n\n```ruby\nUser.where(keyring_id: 1234).find_each do |user|\n    user.keyring_rotate!\nend\n```\n\n### What if I don't use ActiveRecord/Sequel?\n\nYou can also leverage the encryption mechanism of `attr_keyring` totally\ndecoupled from ActiveRecord/Sequel. First, make sure you load `keyring` instead.\nThen you can create a keyring to encrypt/decrypt strings, without even touching\nthe database.\n\n```ruby\nrequire \"keyring\"\n\nkeyring = Keyring.new(\n  {\"1\" =\u003e \"QSXyoiRDPoJmfkJUZ4hJeQ==\"},\n  digest_salt: \"\u003ccustom salt\u003e\"\n)\n\nencrypted, keyring_id, digest = keyring.encrypt(\"super secret\")\n\nputs encrypted\n#=\u003e encrypted: +mOWmIWKMV01nCm076OBnzgPGhWAZqNs8Etaad/0s3I=\n\nputs keyring_id\n#=\u003e 1\n\nputs digest\n#=\u003e e24fe0dea7f9abe8cbb192702578715079689a3e\n\ndecrypted = keyring.decrypt(encrypted, keyring_id)\n\nputs decrypted\n#=\u003e super secret\n```\n\n### Exchange data with Node.js\n\nIf you use Node.js, you may be interested in\n\u003chttps://github.com/fnando/keyring-node\u003e, which is able to read and write\nmessages using the same format.\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run\n`rake test` to run the tests. You can also run `bin/console` for an interactive\nprompt that will allow you to experiment.\n\nTo install this gem onto your local machine, run `bundle exec rake install`. To\nrelease a new version, update the version number in `version.rb`, and then run\n`bundle exec rake release`, which will create a git tag for the version, push\ngit commits and tags, and push the `.gem` file to\n[rubygems.org](https://rubygems.org).\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at\nhttps://github.com/fnando/attr_keyring. This project is intended to be a safe,\nwelcoming space for collaboration, and contributors are expected to adhere to\nthe [Contributor Covenant](http://contributor-covenant.org) code of conduct.\n\n## License\n\nThe gem is available as open source under the terms of the\n[MIT License](https://opensource.org/licenses/MIT).\n\n## Icon\n\nIcon made by [Icongeek26](https://www.flaticon.com/authors/icongeek26) from\n[Flaticon](https://www.flaticon.com/) is licensed by Creative Commons BY 3.0.\n\n## Code of Conduct\n\nEveryone interacting in the attr_keyring project’s codebases, issue trackers,\nchat rooms and mailing lists is expected to follow the\n[code of conduct](https://github.com/fnando/attr_keyring/blob/main/CODE_OF_CONDUCT.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffnando%2Fattr_keyring","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffnando%2Fattr_keyring","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffnando%2Fattr_keyring/lists"}