Skip to content

Commit a9469d3

Browse files
authored
404: Decrypt student password (#451)
## Status - Related to RaspberryPiFoundation/editor-standalone#404 ## What's changed? Passwords from editor-standalone will be encrypted for security, this decrypts them in editor-api ## Steps to perform after deploying to production - Needs encryption key in terraform
1 parent 0bf1bfc commit a9469d3

22 files changed

+281
-34
lines changed

.circleci/config.yml

+11-10
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
jobs:
22
rubocop:
33
docker:
4-
- image: 'cimg/ruby:3.2'
4+
- image: "cimg/ruby:3.2"
55
steps:
66
- checkout
77
- ruby/install-deps
@@ -11,18 +11,18 @@ jobs:
1111

1212
test:
1313
docker:
14-
- image: 'cimg/ruby:3.2-browsers'
15-
- image: 'circleci/postgres:12.0-alpine-ram'
14+
- image: "cimg/ruby:3.2-browsers"
15+
- image: "circleci/postgres:12.0-alpine-ram"
1616
environment:
1717
POSTGRES_DB: choco_cake_test
1818
POSTGRES_PASSWORD: password
1919
POSTGRES_USER: choco
20-
- image: 'circleci/redis:6.2-alpine'
20+
- image: "circleci/redis:6.2-alpine"
2121

2222
environment:
23-
BUNDLE_JOBS: '3'
24-
BUNDLE_RETRY: '3'
25-
PAGER: ''
23+
BUNDLE_JOBS: "3"
24+
BUNDLE_RETRY: "3"
25+
PAGER: ""
2626
POSTGRES_DB: choco_cake_test
2727
POSTGRES_PASSWORD: password
2828
POSTGRES_USER: choco
@@ -31,19 +31,20 @@ jobs:
3131
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY: primary-key
3232
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY: deterministic-key
3333
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT: derivation-salt
34+
EDITOR_ENCRYPTION_KEY: a1b2c3d4e5f67890123456789abcdef0123456789abcdef0123456789abcdef0
3435
steps:
3536
- checkout
3637
- browser-tools/install-firefox
3738
- ruby/install-deps:
3839
key: gems-v2-
3940
- run:
40-
command: 'dockerize -wait tcp://localhost:5432 -timeout 1m'
41+
command: "dockerize -wait tcp://localhost:5432 -timeout 1m"
4142
name: Wait for DB
4243
- run:
43-
command: 'sudo apt-get update && sudo apt-get install --yes --no-install-recommends postgresql-client jq curl imagemagick'
44+
command: "sudo apt-get update && sudo apt-get install --yes --no-install-recommends postgresql-client jq curl imagemagick"
4445
name: Install postgres client, jq, curl, imagemagick
4546
- run:
46-
command: 'bin/rails db:setup --trace'
47+
command: "bin/rails db:setup --trace"
4748
name: Database setup
4849
- ruby/rspec-test
4950
- store_artifacts:

.env.example

+2
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,5 @@ PROFILE_API_KEY=test # This has to match the value set in Profile (https://githu
4545
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=primary-key
4646
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=deterministic-key
4747
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=derivation-salt
48+
49+
EDITOR_ENCRYPTION_KEY=a1b2c3d4e5f67890123456789abcdef0123456789abcdef0123456789abcdef0

Gemfile

-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ gem 'postmark-rails'
3333
gem 'puma', '~> 6'
3434
gem 'rack-cors'
3535
gem 'rails', '~> 7.1'
36-
gem 'roo'
3736
gem 'scout_apm'
3837
gem 'sentry-rails'
3938

Gemfile.lock

-4
Original file line numberDiff line numberDiff line change
@@ -372,9 +372,6 @@ GEM
372372
rack (>= 1.4)
373373
rexml (3.3.0)
374374
strscan
375-
roo (2.10.1)
376-
nokogiri (~> 1)
377-
rubyzip (>= 1.3.0, < 3.0.0)
378375
rspec (3.12.0)
379376
rspec-core (~> 3.12.0)
380377
rspec-expectations (~> 3.12.0)
@@ -551,7 +548,6 @@ DEPENDENCIES
551548
puma (~> 6)
552549
rack-cors
553550
rails (~> 7.1)
554-
roo
555551
rspec
556552
rspec-rails
557553
rspec_junit_formatter

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,13 @@ If needed manually the following task will create all projects:
7171

7272
`docker compose run --rm api rails projects:create_all`
7373

74-
For classroom management the following scenarios modelled by the tasks:
74+
For CEfE the following scenarios are modelled by the tasks:
7575

7676
`docker compose run --rm api rails for_education:seed_an_unverified_school` - seeds an unverified school to test the onboarding flow
7777
`docker compose run --rm api rails for_education:seed_a_verified_school` - seeds only a verified school
7878
`docker compose run --rm api rails for_education:seed_a_school_with_lessons_and_students` - seeds a school with a class, two lessons, a project in each, and two students
7979

80-
To clear classroom management data the following cmd will remove the school associated with the `[email protected]` user, and associated school data:
80+
To clear CEfE data the following cmd will remove the school associated with the `[email protected]` user, and associated school data:
8181

8282
`rails for_education:destroy_seed_data`
8383

app/jobs/create_students_job.rb

+4-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ def self.attempt_perform_later(school_id:, students:, token:, user_id:)
2828
end
2929

3030
def perform(school_id:, students:, token:)
31-
students = Array(students)
31+
students = Array(students).map do |student|
32+
student[:password] = DecryptionHelpers.decrypt_password(student[:password])
33+
student
34+
end
3235

3336
responses = ProfileApiClient.create_school_students(token:, students:, school_id:)
3437
return if responses[:created].blank?

config/application.rb

+3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ class Application < Rails::Application
3030

3131
config.autoload_paths << "#{root}/lib/concepts"
3232
Rails.autoloaders.main.collapse('lib/concepts/*/operations')
33+
34+
config.autoload_paths << "#{root}/lib/helpers"
35+
3336
config.autoload_lib(ignore: %w[assets tasks])
3437

3538
# Configuration for the application, engines, and railties goes here.

lib/concepts/school_student/create.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ def call(school:, school_student_params:, token:)
1818
def create_student(school, school_student_params, token)
1919
school_id = school.id
2020
username = school_student_params.fetch(:username)
21-
password = school_student_params.fetch(:password)
21+
encrypted_password = school_student_params.fetch(:password)
22+
password = DecryptionHelpers.decrypt_password(encrypted_password)
2223
name = school_student_params.fetch(:name)
2324

2425
validate(school:, username:, password:, name:)

lib/concepts/school_student/create_batch.rb

-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
# frozen_string_literal: true
22

3-
require 'roo'
4-
53
module SchoolStudent
64
class CreateBatch
75
class << self

lib/concepts/school_student/update.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ def call(school:, student_id:, school_student_params:, token:)
1717

1818
def update_student(school, student_id, school_student_params, token)
1919
username = school_student_params.fetch(:username, nil)
20-
password = school_student_params.fetch(:password, nil)
20+
encrypted_password = school_student_params.fetch(:password, nil)
21+
password = DecryptionHelpers.decrypt_password(encrypted_password)
2122
name = school_student_params.fetch(:name, nil)
2223

2324
validate(username:, password:, name:)

lib/helpers/decryption_helpers.rb

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# frozen_string_literal: true
2+
3+
require 'openssl'
4+
require 'base64'
5+
6+
class DecryptionHelpers
7+
def self.decrypt_password(encrypted_password)
8+
hex_key = ENV.fetch('EDITOR_ENCRYPTION_KEY')
9+
key = [hex_key].pack('H*') # Convert the hex key to binary
10+
11+
begin
12+
cipher = OpenSSL::Cipher.new('aes-256-cbc')
13+
cipher.decrypt
14+
cipher.key = key
15+
16+
encrypted_data = Base64.decode64(encrypted_password)
17+
iv = encrypted_data[0, 16]
18+
encrypted_password = encrypted_data[16..]
19+
20+
cipher.iv = iv
21+
cipher.update(encrypted_password) + cipher.final
22+
rescue StandardError => e
23+
raise "Decryption failed: #{e.message}"
24+
end
25+
end
26+
end
File renamed without changes.

spec/concepts/school_student/create_spec.rb

+3-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
let(:school_student_params) do
1111
{
1212
username: 'student-to-create',
13-
password: 'at-least-8-characters',
13+
password: 'SaoXlDBAyiAFoMH3VsddhdA7JWnM8P8by1wOjBUWH2g=',
1414
name: 'School Student'
1515
}
1616
end
@@ -29,7 +29,7 @@
2929

3030
# TODO: Replace with WebMock assertion once the profile API has been built.
3131
expect(ProfileApiClient).to have_received(:create_school_student)
32-
.with(token:, username: 'student-to-create', password: 'at-least-8-characters', name: 'School Student', school_id: school.id)
32+
.with(token:, username: 'student-to-create', password: 'Student2024', name: 'School Student', school_id: school.id)
3333
end
3434

3535
it 'creates a role associating the student with the school' do
@@ -46,7 +46,7 @@
4646
let(:school_student_params) do
4747
{
4848
username: '',
49-
password: 'at-least-8-characters',
49+
password: 'SaoXlDBAyiAFoMH3VsddhdA7JWnM8P8by1wOjBUWH2g=',
5050
name: 'School Student'
5151
}
5252
end

spec/concepts/school_student/update_spec.rb

+3-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
let(:school_student_params) do
1111
{
1212
username: 'new-username',
13-
password: 'new-password',
13+
password: 'SaoXlDBAyiAFoMH3VsddhdA7JWnM8P8by1wOjBUWH2g=',
1414
name: 'New Name'
1515
}
1616
end
@@ -29,14 +29,14 @@
2929

3030
# TODO: Replace with WebMock assertion once the profile API has been built.
3131
expect(ProfileApiClient).to have_received(:update_school_student)
32-
.with(token:, username: 'new-username', password: 'new-password', name: 'New Name', school_id: school.id, student_id:)
32+
.with(token:, username: 'new-username', password: 'Student2024', name: 'New Name', school_id: school.id, student_id:)
3333
end
3434

3535
context 'when updating fails' do
3636
let(:school_student_params) do
3737
{
3838
username: ' ',
39-
password: 'new-password',
39+
password: 'SaoXlDBAyiAFoMH3VsddhdA7JWnM8P8by1wOjBUWH2g=',
4040
name: 'New Name'
4141
}
4242
end

spec/features/school_student/creating_a_batch_of_school_students_spec.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@
2424
school_students: [
2525
{
2626
username: 'student-to-create',
27-
password: 'at-least-8-characters',
27+
password: 'SaoXlDBAyiAFoMH3VsddhdA7JWnM8P8by1wOjBUWH2g=',
2828
name: 'School Student'
2929
},
3030
{
3131
username: 'second-student-to-create',
32-
password: 'at-least-8-characters',
32+
password: 'SaoXlDBAyiAFoMH3VsddhdA7JWnM8P8by1wOjBUWH2g=',
3333
name: 'School Student 2'
3434
}
3535
]

spec/features/school_student/creating_a_school_student_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
{
1818
school_student: {
1919
username: 'student123',
20-
password: 'at-least-8-characters',
20+
password: 'SaoXlDBAyiAFoMH3VsddhdA7JWnM8P8by1wOjBUWH2g=',
2121
name: 'School Student'
2222
}
2323
}

spec/features/school_student/updating_a_school_student_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
{
1919
school_student: {
2020
username: 'new-username',
21-
password: 'new-password',
21+
password: 'SaoXlDBAyiAFoMH3VsddhdA7JWnM8P8by1wOjBUWH2g=',
2222
name: 'New Name'
2323
}
2424
}

spec/jobs/create_students_job_spec.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
let(:students) do
1313
[{
1414
username: 'student-to-create',
15-
password: 'at-least-8-characters',
15+
password: 'SaoXlDBAyiAFoMH3VsddhdA7JWnM8P8by1wOjBUWH2g=',
1616
name: 'School Student'
1717
}]
1818
end
@@ -33,7 +33,7 @@
3333
described_class.perform_now(school_id: school.id, students:, token:)
3434

3535
expect(ProfileApiClient).to have_received(:create_school_students)
36-
.with(token:, students: [{ username: 'student-to-create', password: 'at-least-8-characters', name: 'School Student' }], school_id: school.id)
36+
.with(token:, students: [{ username: 'student-to-create', password: 'Student2024', name: 'School Student' }], school_id: school.id)
3737
end
3838

3939
it 'creates a new student role' do
File renamed without changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
require 'openssl'
5+
require 'base64'
6+
7+
RSpec.describe DecryptionHelpers do
8+
let(:key) { 'a1b2c3d4e5f67890123456789abcdef0123456789abcdef0123456789abcdef0' } # 256-bit key in hex
9+
let(:password) { 'Student2024' }
10+
let(:encrypted_password) { 'SaoXlDBAyiAFoMH3VsddhdA7JWnM8P8by1wOjBUWH2g=' } # An encrypted password
11+
12+
before do
13+
allow(ENV).to receive(:fetch).with('EDITOR_ENCRYPTION_KEY').and_return(key)
14+
end
15+
16+
it 'decrypts the password successfully' do
17+
expect(described_class.decrypt_password(encrypted_password)).to eq(password)
18+
end
19+
20+
it 'raises an error with an incorrect key' do
21+
allow(ENV).to receive(:fetch).with('EDITOR_ENCRYPTION_KEY').and_return('b' * 64)
22+
expect { described_class.decrypt_password(encrypted_password) }.to raise_error(RuntimeError, /Decryption failed/)
23+
end
24+
25+
it 'raises an error with invalid encrypted data' do
26+
invalid_encrypted_password = Base64.encode64('invalid_data')
27+
expect { described_class.decrypt_password(invalid_encrypted_password) }.to raise_error(RuntimeError, /Decryption failed/)
28+
end
29+
end

0 commit comments

Comments
 (0)