panco’s blog

興味が沸いたことを書く

'sqlite3.Row' の型変換

PythonSQL (SQLite3) の取得結果を使った処理するうえで、その取得結果を型変換する必要があった。どのように対処したか残しておく。
(やりたいことはできたのだけど、これが最適解なのかはわからない。。)

実行環境

参照するテーブルの状態

例として、users テーブルから任意の値を取得し、その結果を型変換する。
users テーブルの状態は以下の通り。

user_id user_name password user_role updated_at
1 panco pbkdf2:sha256:260000$aA42TAOrOJ1mYet1$16b8aa3305971337a49db43b1ff9624589d5963c10c546941ec7692ac36d0228 0 2023-10-14 16:39:08

ケース ① int 型への変換

やりたいこと

  • users テーブルのレコード数を取得し、その値を int 型で扱う

サンプルコード

hoge.py

import sqlite3

# DB 指定
DATABASE='pog-draft-project/pog.db'
# DB 接続
def connect_db():
    rv = sqlite3.connect(DATABASE)
    rv.row_factory = sqlite3.Row
    return rv

# int への変換
def get_int_data():
    # DB 接続
    con = connect_db()
    cur = con.cursor()
    sql = "SELECT count (*) FROM users WHERE user_name = 'panco'"
    res = cur.execute(sql).fetchone()
    # 変換前の値・型
    print(res)
    print(type(res))
    # map で str 変換してから int へ変換
    res_str = map(str, res)
    res = int(''.join(res_str))
    # 変換後の値・型
    print(res)
    print(type(res))

get_int_data()

実行結果

# 変換前の値・型
<sqlite3.Row object at 0x0000023B73C42E60>
<class 'sqlite3.Row'>
# 変換後の値・型
1
<class 'int'>

ケース ② str 型への変換

やりたいこと

  • users テーブルから user_name を取得し、その値を str 型で扱う

サンプルコード

hoge.py

import sqlite3

# DB 指定
DATABASE='pog-draft-project/pog.db'
# DB 接続
def connect_db():
    rv = sqlite3.connect(DATABASE)
    rv.row_factory = sqlite3.Row
    return rv

# str への変換
def get_str_data():
    # DB 接続
    con = connect_db()
    cur = con.cursor()
    sql = "SELECT user_name FROM users WHERE user_id = 1"
    res = cur.execute(sql).fetchone()
    # 変換前の値・型
    print(res)
    print(type(res))
    # str へ変換
    res = ''.join(res)
    # 変換後の値・型
    print(res)
    print(type(res))

get_str_data()

実行結果

# 変換前の値・型
<sqlite3.Row object at 0x0000024F34A13370>
<class 'sqlite3.Row'>
# 変換後の値・型
panco
<class 'str'>

閑話 着ぐるみ恐怖症

お題「わたしは ○○ 恐怖症」

ゆるネタ。

遊園地は好きだが着ぐるみ恐怖症だ。
YouTube でディズニーのパレードを見るだけでもぞわっとするレベルだ。
苦手意識が強すぎて、半径 40m 程度であれば存在を察知し、鉢合わせを回避する能力が備わっている。とはいえ、商店街に突如として現れる着ぐるみは心臓に悪い。(中の人お疲れ様です、という気持ちは多少はある)

レベル別に耐えられる着ぐるみとそうでない着ぐるみを紹介する。共感してくれる人がいたら嬉しい。
画像は著作権とかいろいろありそうなので貼らない。

恐怖レベルの定義と代表的な着ぐるみたち

  • 基本的に怖くて遭遇したくないため、定義に書かれたものはすべて「相当な覚悟を決めた上」での行為であることを前提とする。
  • 地上で遭遇することを前提とする。着ぐるみがフロートなどに乗っているケースは今回は対象外。
  • この記事を書くために、YouTube でグリーティング動画を頑張って見たよ。
恐怖レベル   定義
1 隣に並んで写真を撮れる。目を合わせる・手をつなぐなどの接触は NG
2 着ぐるみを中心に半径 5m 以内の位置で直視できる
3 半径 5 ~ 15m の位置で眺められる
4 半径 15 ~ 30m の位置で眺められる
5 どう頑張っても無理

恐怖レベル 1 の着ぐるみ

  • ダッフィー

恐怖レベル 2 の着ぐるみ

恐怖レベル 3 の着ぐるみ

  • ぷーさん
  • ティガー

恐怖レベル 4 の着ぐるみ

恐怖レベル 5 の着ぐるみ

何がそんなに怖いのか

「なぜ怖いのか」物心ついたときから考えているが答えが出ない。怖いものは怖いのだ。それではなかなか人に伝わらないため、「何に怖さを感じるのか」を言語化したことがあった。結果は以下の通り。

怖いと感じる着ぐるみの特徴

パーツ   特徴
身長 160cm 以上(自分と同等かそれ以上)
体型 人型
人の原型をとどめていないフォルムのほうが耐えられる
頭部 頭だけが体に対して異常に大きい
白目と黒目で構成されている
黒一色のつぶらな瞳は OK
肌質 プラスチックのようなすべすべして硬そうな素材
鼻・口 大きかったり突出したりしている(ドナルドダックの口とか怖い)
腕・脚 人の手足の名残を感じる(ミニーが特にそれ)
人が得体のしれない何か(着ぐるみ)に食べられてしまったように感じて恐怖する
指があるタイプ(人の名残を感じるため)

werkzeug.security.check_password_hash() について

ログイン処理(認証)の実装で躓いた点を書く。

実行環境

処理の流れ

  1. 新規登録画面からユーザ ID、パスワードを入力
  2. 新規登録ボタン押下
  3. 入力チェック OK なら、パスワードをハッシュ化してテーブルへ登録しログイン画面へ遷移(NG なら新規登録画面へ遷移)
  4. ログイン画面にユーザ ID、パスワードを入力
  5. 認証して OK なら、別の画面に遷移(NG ならログイン画面へ遷移)

5 が今回作ったところ。認証には werkzeug.security.check_password_hash() を使う

出来上がったソースコードたち

一応期待通りに動いている。
app.py

from  flask import Flask, render_template, redirect, g, request
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, EqualTo
import sqlite3
from werkzeug.security import generate_password_hash, check_password_hash
from IPython.core.display import display

app = Flask(__name__)

# 秘密鍵設定
app.config['SECRET_KEY'] = 'mysecretkey'

# DB 指定
DATABASE='pog-draft-project/pog.db'
# DB 接続
def connect_db():
    rv = sqlite3.connect(DATABASE)
    rv.row_factory = sqlite3.Row
    return rv
# DB 取得
def get_db():
    if not hasattr(g,'sqlite_db'):
        g.sqlite_db = connect_db()
    return g.sqlite_db

# 新規登録の form クラス
class registrationForm(FlaskForm):
    # 各項目と入力チェックの内容を設定
    username = StringField('ユーザ名', validators=[DataRequired()])
    password = PasswordField('パスワード', validators=[DataRequired()])
    pw_check = PasswordField('パスワード確認', validators=[DataRequired(), EqualTo('password')])
    submit = SubmitField('新規登録')

# 新規登録処理
@app.route('/index', methods=['GET','POST'])
def index():
    form = registrationForm()
    # 入力チェックを実施して問題なければ users テーブルに格納
    if request.method == 'POST':
        if form.validate_on_submit():
            # パスワードをハッシュ値に変換
            pw_hash = generate_password_hash(form.password.data, method='pbkdf2:sha256')
            # DB へユーザ情報を登録
            con = get_db()
            cur = con.cursor()
            sql = "INSERT INTO users(user_name, password) VALUES(?, ?)"
            param = [form.username.data, pw_hash]
            cur.execute(sql, param)
            con.commit()
            con.close()
            return redirect('/login')
        return render_template('index.html', form=form)
    return render_template('index.html', form=form)

# ログイン画面の form クラス
class loginForm(FlaskForm):
    # 各項目と入力チェックの内容を設定
    username = StringField('ユーザ名', validators=[DataRequired()])
    password = PasswordField('パスワード', validators=[DataRequired()])
    submit = SubmitField('ログイン')

# ログイン処理
@app.route('/login', methods=['GET','POST'])
def login():
    form = loginForm()
    if request.method == 'POST':
        # Users テーブルから対象ユーザの password を取得
        con = get_db()
        cur = con.cursor()
        sql = "SELECT password FROM users WHERE user_name = ?"
        param = form.username.data
        res = cur.execute(sql, (param,)).fetchone()
        # tuple -> string
        res = ''.join(res)
        con.close()

        # パスワード認証
        pw_auth = check_password_hash(pwhash=res, password=form.password.data)
        print(pw_auth)
        if pw_auth == True:
            # 認証 OK の場合
            return redirect('/selection')
        msg = 'ログインエラー:ユーザ名またはパスワードが一致しません'
        return render_template('login.html', form=form, msg=msg)
    return render_template('login.html', form=form)

if __name__ == "__main__":
    app.run(debug=True)

index.html

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>新規登録</title>
  </head>
  <body>
    <h2>新規登録</h2>
    <div>
      <form method="POST">
        {{ form.hidden_tag() }}
        <!-- 入力フォーム設定 -->
        <div>{{ form.username.label }} {{ form.username() }}</div>
        <div>{{ form.password.label }} {{ form.password() }}</div>
        <div>
          {{ form.pw_check.label }} {{ form.pw_check() }} {{ form.pw_check.error
          }}
        </div>
        {{ form.submit() }}
        <div>
          {% if form.pw_check.errors %}
          <ul>
            {% for error in form.pw_check.errors %}
            <li>{{ form.pw_check.label ~' '~ error }}</li>
            {% endfor %}
          </ul>
          {% endif %}
        </div>
        <div><a href="/login">ログイン画面はこちら</a></div>
      </form>
    </div>
  </body>
</html>

login.html

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>ログイン</title>
  </head>
  <body>
    <h2>ログイン</h2>
    <div>
      <form method="POST">
        <!-- 入力フォーム設定 -->
        <div>{{ form.username.label }} {{ form.username() }}</div>
        <div>{{ form.password.label }} {{ form.password() }}</div>
        <div>{{ form.submit() }}</div>
    <div><a href="/index">新規登録画面はこちら</a></div>
    <div>{{ msg }}</div>
  </form>
  </body>
</html>

werkzeug.security.check_password_hash() について

generate_password_hash() とセットで使う。generate_password_hash() については、以下記事を参照。

pancokeiba.hatenablog.com

check_password_hash() のパラメータと戻り値は以下の通り。

Parameters: pwhash (str) – a hashed string like returned by generate_password_hash().

password (str) – the plaintext password to compare against the hash.

Return type: bool

Utilities — Werkzeug Documentation (2.2.x)

  • 第一引数: 新規登録処理で generate_password_hash()でハッシュ化してテーブルに登録したパスワードの値
  • 第二引数: ログイン画面でユーザが入力した平文パスワード
    check_password_hash() が第二引数の平文パスワードをハッシュ化し、第一引数のハッシュ値(つまり正解の値)と比較してくれる。比較した結果、2 つの値が一致していれば True, 不一致であれば False を返してくれる。シンプルな仕組みで分かりやすい。

ログイン処理の実装で躓いた点

前提

以下、1 ~ 3 が実行済みであり、テーブルに以下のレコードが格納されていること。

  1. 新規登録画面からユーザ ID、パスワードを入力
  2. 新規登録ボタン押下
  3. 入力チェック OK なら、パスワードをハッシュ化してテーブルへ登録しログイン画面へ遷移(NG なら新規登録画面へ遷移)

テーブル状態(正常終了時)

user_id user_name password user_role updated_at
1 panco pbkdf2:sha256:260000$aA42TAOrOJ1mYet1$16b8aa3305971337a49db43b1ff9624589d5963c10c546941ec7692ac36d0228 0 2023-10-14 16:39:08

躓き ① 第一引数の取得で ProgrammingError

sqlite3.ProgrammingError: Incorrect number of bindings supplied. The current statement uses 1, and there are 5 supplied.

ProgrammingError
ProgrammingError

ProgrammingError になるコード

if request.method == 'POST':
    # Users テーブルから対象ユーザの password を取得
    con = get_db()
    cur = con.cursor()
    sql = "SELECT password FROM users WHERE user_name = ?"
    param = form.username.data
    res = cur.execute(sql, param).fetchone()
    con.close()

修正後のコード
ProgrammingError は解消したが、後述する AttributeError が発生した。

if request.method == 'POST':
    # Users テーブルから対象ユーザの password を取得
    con = get_db()
    cur = con.cursor()
    sql = "SELECT password FROM users WHERE user_name = ?"
    param = form.username.data
 # (sql, (param,)) と書く必要がある
    res = cur.execute(sql, (param,)).fetchone()
    con.close()

躓き ② 第一引数の取得で AttributeError

AttributeError: 'sqlite3.Row' object has no attribute 'count'

AttributeError
AttributeError

AttributeError になるコード

if request.method == 'POST':
    # Users テーブルから対象ユーザの password を取得
    con = get_db()
    cur = con.cursor()
    sql = "SELECT password FROM users WHERE user_name = ?"
    param = form.username.data
 # (sql, (param,)) と書く必要がある
    res = cur.execute(sql, (param,)).fetchone()
    con.close()

テーブルに格納された password は SQL で取得できているが、取得結果(変数 res)の値が以下のようになっていた。

<sqlite3.Row object at 0x0000019D20857970>

期待値は以下の文字列となるが、'sqlite3.Row'という異なる型で出力してしまった。str 型ではないため count が使えず怒られている。

pbkdf2:sha256:260000$aA42TAOrOJ1mYet1$16b8aa3305971337a49db43b1ff9624589d5963c10c546941ec7692ac36d0228

補足
count は check_password_hash() で、第一引数の "$" の数を数える際に使っている。
werkzeug>security.py

def check_password_hash(pwhash: str, password: str) -> bool:
    if pwhash.count("$") < 2:
        return False

修正後のコード

if request.method == 'POST':
    # Users テーブルから対象ユーザの password を取得
    con = get_db()
    cur = con.cursor()
    sql = "SELECT password FROM users WHERE user_name = ?"
    param = form.username.data
    res = cur.execute(sql, (param,)).fetchone()
    # tuple -> str に型変換
    res = ''.join(res)
    con.close()

型を str に変換することで解消した。
こうすることで、変数 res には期待通りに以下の値が入った。

pbkdf2:sha256:260000$aA42TAOrOJ1mYet1$16b8aa3305971337a49db43b1ff9624589d5963c10c546941ec7692ac36d0228

以上で、正常に第一引数を渡せた。第二引数はログイン画面から入力されたパスワードを渡すだけのため特に引っかかる点はなし。無事に True or False が返るようになった。

werkzeug.security.generate_password_hash() について調べた

趣味の個人開発でとあるログイン機能を設計・実装している。

「テーブルに平文パスワードを入れるのはだめだよな。普通どうするのかな」という疑問から調べてみると、generate_password_hash() と check_password_hash() に出会った。この記事では、generate_password_hash() について理解したことを書く。

事前知識は皆無であるため公式ドキュメントを参照しつつ、理解できなかった点は ChatGPT の力を借りた。

実行環境

  • python 3.10.13
  • werkzeug 2.2.3

generate_password_hash() とは

公式ドキュメント
Utilities — Werkzeug Documentation (2.2.x)

簡潔に言うと、平文のパスワードを何らかのハッシュアルゴリズム(+ソルト)を使ってハッシュ値にしてくれるもの。

This method can not generate unsalted passwords but it is possible to set param method=’plain’ in order to enforce plaintext passwords.

上記の通り、ソルトを生成してハッシュ値を出力する仕様となっている。"method=’plain’" については method='plain' で記載する。

パラメータ、戻り値は以下の通り。(公式ドキュメントからの抜粋)

Parameters: password (str) – the password to hash.

method (str) – the hash method to use (one that hashlib supports). Can optionally be in the format pbkdf2:method:iterations to enable PBKDF2.

salt_length (int) – the length of the salt in letters.

Return type: str

戻り値の形式は以下の通り。

method$salt$hash

とりあえず適当にコードを書いて実行してみる。

実行コード

from werkzeug.security import generate_password_hash

# 平文のパスワード
pw = 'panco2355'
# generate_password_hash() デフォルト
pw_hash = generate_password_hash(pw)

print('pw =' + pw)
print('pw_hash = ' + pw_hash)

実行結果

pw = panco2355
pw_hash = pbkdf2:sha256:260000$O0B6LXDYDeichV3Q$00427a87a7223395a3c2a407df790ff3c17571b2ef4eb20dccd57a1204ef9407

平文の panco2355 はいいとして、pw_hash の中身を見ていく。

method$salt$hash

の形式に当てはめると、

method salt hash
pbkdf2:sha256:260000 O0B6LXDYDeichV3Q 00427a87a7223395a3c2a407df790ff3c17571b2ef4eb20dccd57a1204ef9407

となる。第一引数(つまり password)しか設定しない場合、method='pbkdf2:sha256'、salt_length=16 となる。

method で設定できるハッシュアルゴリズム

werkzeug 2.2.x で使用できる method は hashlib がサポートしているものとのこと。 Python 3.10.x だと、少なくとも以下のハッシュアルゴリズムが使用できるらしい。

Constructors for hash algorithms that are always present in this module are sha1(), sha224(), sha256(), sha384(), sha512(), blake2b(), and blake2s().

hashlib — Secure hashes and message digests — Python 3.10.13 documentation

method "the format pbkdf2:method:iterations" って何?

公式ドキュメントを読んでいて理解できなかった点がある。以下の一文。

If PBKDF2 is wanted it can be enabled by setting the method to pbkdf2:method:iterations where iterations is optional:

" by setting the method to pbkdf2:method:iterations" が何を言っているのか全然わからない。どう実装するのか、それによって何が変わるのかが理解できない。
ということで、ChatGPT にこの一文をまるっと投げて解説してもらった。結果はこちら。

ChatGPT-3.5 の解説

ひとまず、pbkdf2、method、iterations の概念が理解できた。実装例まである。(pbkdf2 の原理も理解したいところだが、夜が明けてしまいそうなので一旦スルーする)

Chat GPT の解説を踏まえ、以下のコードを実行した。

実行コード

from werkzeug.security import generate_password_hash

# 平文のパスワード
pw = 'panco2355'
# generate_password_hash() デフォルト
pw_hash = generate_password_hash(pw)
# method='pbkdf2:sha256:1000'
pw_hash_pbkdf2 = generate_password_hash(pw, method='pbkdf2:sha256:1000')

print('pw = ' + pw)
print('pw_hash = ' + pw_hash)
print('pw_hash_pbkdf2 = ' + pw_hash_pbkdf2)

実行結果

pw = panco2355
pw_hash = pbkdf2:sha256:260000$vDyoTiqgkmBSezIs$5988047da6b68cffb115da078f278850608c17ca6de53a0ee178727111db97f0
pw_hash_pbkdf2 = pbkdf2:sha256:1000$i0zIckuEmddoEWNf$15da30ba7f19a3103b421b08da9a2b6e394b8c799c6ca8fb8ec80c9f644f61a0

変数 pw_hash_pbkdf2 の値から、第二引数に設定した「pbkdf2:sha256:1000」が使われていることがわかる。デフォルトだと 260,000 回繰り返していたのか。なるほど。

method='sha256'

とあるサンプルコードにて、method='sha256' で設定しているものがあった。その場合、戻り値がどうなるかも見てみる。

実行コード

from werkzeug.security import generate_password_hash, check_password_hash

# 平文のパスワード
pw = 'panco2355'
# generate_password_hash() デフォルト
pw_hash = generate_password_hash(pw)
# method='pbkdf2'
pw_hash_pbkdf2 = generate_password_hash(pw, method='pbkdf2:sha256:1000')
# method='sha256'
pw_hash_sha256 = generate_password_hash(pw, method='sha256')

print('pw = ' + pw)
print('pw_hash = ' + pw_hash)
print('pw_hash_pbkdf2 = ' + pw_hash_pbkdf2)
print('pw_hash_sha256' + pw_hash_sha256)

実行結果

pw = panco2355
pw_hash = pbkdf2:sha256:260000$N7aRYni2ZDia67b5$7b28748685ab69f6bdc18d49b01970949d31bfa08727ca00d986194c953e40a6
pw_hash_pbkdf2 = pbkdf2:sha256:1000$HzAAy6RJqZFbYsgJ$ba6ab5aa7e8c6ffec9ad8cac342008e303b8459c1c1a59eccb17a1091b8bd200
pw_hash_sha256 = sha256$i3NHoX8sEWipcAub$36e7481117406ca0c10537c02b6e99fa59fb626f7573551304528159be662efd

ソルトを生成してハッシュ値を出力することはわかった。そして、PBKDF2 を有効化すると、iterations に設定した任意の回数を繰り返して何かしらを行い、ハッシュ値を出力しているということもわかってきた。(おそらく自分の基礎的な知識がなさすぎる)

method='plain'

使い道がわからないが、method='plain' とすることで平文のパスワードを出力することもできた。

実行コード

from werkzeug.security import generate_password_hash, check_password_hash

# 平文のパスワード
pw = 'panco2355'
# generate_password_hash() デフォルト
pw_hash = generate_password_hash(pw)
# method='pbkdf2'
pw_hash_pbkdf2 = generate_password_hash(pw, method='pbkdf2:sha256:1000')
# method='sha256'
pw_hash_sha256 = generate_password_hash(pw, method='sha256')
# method='plain'
pw_hash_plain = generate_password_hash(pw, method='plain')

print('pw = ' + pw)
print('pw_hash = ' + pw_hash)
print('pw_hash_pbkdf2 = ' + pw_hash_pbkdf2)
print('pw_hash_sha256 = ' + pw_hash_sha256)
print('pw_hash_plain = ' + pw_hash_plain)

実行結果

pw = panco2355
pw_hash = pbkdf2:sha256:260000$r6KekC7Nk7XqDbUi$1ed2202960fe81a88796d574162669dab8cc6ab12a3eb858645bbd4d8bdf9851
pw_hash_pbkdf2 = pbkdf2:sha256:1000$MXAZ6floiejlxW4Y$c406a48ea1dd8312460cd1bbf5088b6c0bc1e8446271fba67608d4a011772040
pw_hash_sha256 = sha256$RkpCXxhoBEVTEdQ5$921b331a536aa5f806a2d7a78977c7804614f8cb6f1766144ddf5265a910bb6a
pw_hash_plain = plain$$panco2355

method='plain' の場合、ハッシュの概念がないためソルト生成もない。だから「plain$$panco2355」の「$」で挟まれた部分(ソルトの値が入る位置)が空になっている。

salt_length=0 にしたらどうなるのか

何度実行しても同じ平文からは同じハッシュ値が出るはず、と思ったが ValueError で怒られた。

実行コード

from werkzeug.security import generate_password_hash, check_password_hash

# 平文のパスワード
pw = 'panco2355'
# method='salt0'
for i in range(5):
    pw_hash_salt0 = generate_password_hash(pw, method='sha256',salt_length=0)
    print(str(i+1) + '回目' + pw_hash_salt0)

実行結果

ValueError: Salt length must be positive

werkzeug > security.py のここではじかれていた。

def gen_salt(length: int) -> str:
    """Generate a random string of SALT_CHARS with specified ``length``."""
    if length <= 0:
        raise ValueError("Salt length must be positive")

    return "".join(secrets.choice(SALT_CHARS) for _ in range(length))

余談だが、security.py で以下のコードが書かれているため、method='pbkdf2:sha256' のように method に iterations を設定しない場合、デフォルト値の 260000 が採用されることがわかった。

SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
DEFAULT_PBKDF2_ITERATIONS = 260000

werkzeug/src/werkzeug/security.py at 2.2.x · pallets/werkzeug · GitHub

ソルトを使わないハッシュアルゴリズム

技術の進歩に逆行していることは認識しつつ、気になってしまったので軽く ChatGPT に聞いてみた。

たしかに MD5 は非推奨というのはどこかのサイトで見た。やってみよう。
generate_password_hash() では、ソルトを使わないハッシュ値生成はできない(method='plain'と指定した場合を除く)ため、hashlib.md5() を使った。

実行コード

import hashlib

# 平文のパスワード
pw = 'panco2355'

for i in range(5):
    pw_hash_md5 = hashlib.md5(pw.encode()).hexdigest()
    print(str(i+1) + '回目' + str(pw_hash_md5))

実行結果

1回目1dd93192408bd95cae7c7af78de8b6c4
2回目1dd93192408bd95cae7c7af78de8b6c4
3回目1dd93192408bd95cae7c7af78de8b6c4
4回目1dd93192408bd95cae7c7af78de8b6c4
5回目1dd93192408bd95cae7c7af78de8b6c4

当たり前だが本当に同じハッシュ値になった。

PBKDF2 とは

夜明けが近づいてきたため、PBKDF2 についてはまた今度、と思ったがChatGPTに聞いてしまった。

PBKDF2(Password-Based Key Derivation Function 2)は、パスワードから導出されたキーを生成するためのハッシュ関数です。iterations(反復回数)パラメータは、PBKDF2がパスワードから導出されたキーの強度を調整するために使用されます。高いiterations値を使用することで、生成されたキーの強度が向上し、攻撃者による総当たり攻撃からの保護が強化されます。

PBKDF2は、暗号学的に安全なハッシュ関数の特性を持つため、総当たり攻撃に対して強力な保護を提供します。しかし、PBKDF2自体は高速に動作するため、単純なパスワードから生成されたキーは総当たり攻撃に対して脆弱です。iterationsパラメータは、PBKDF2を複数回繰り返すことで、攻撃者に対する防御を向上させるものです。

具体的には、PBKDF2は以下のように動作します:

  1. パスワードとソルト(ランダムなデータ)を組み合わせます。
  2. ハッシュ関数(通常はSHA-1やSHA-256など)を使用して、この組み合わせをハッシュ化します。
  3. 1および2のステップをiterations回繰り返します。

iterationsの値を高く設定することで、ハッシュ化プロセスが遅くなり、総当たり攻撃に対する耐性が向上します。したがって、高いiterations値はセキュリティを向上させるために重要です。ただし、iterationsの増加にはコンピュータの処理負荷が増加し、認証プロセスが遅くなるというコストが伴います。適切なiterationsの値は、セキュリティ要件とシステムパフォーマ>ンスを考慮して選択されます。

SQLite で躓いたこと(sqlite3.OperationalError: no such table)

またまた備忘録。

やりたいこと

作成済みのテーブルから任意のレコードを取得し、ブラウザで画面表示させること。
見た目がダサすぎるのはさておき。

ディレクトリ構成

C:.
│  pog.db
│  test.py
│
│
└─templates
        test.html

sqlite3.OperationalError: no such table: pog_lists

エラー画面

以下のコードを実行したところ、pog_lists などというテーブルはないと怒られた。pog.db に pog_lists テーブルは作成済みであり、SQL 単体で実行すると正常に取得できた。

実行したコード(NG)

test.py

from  flask import Flask, render_template, g
import sqlite3
DATABASE='pog.db'

app = Flask(__name__)

# database 接続
def connect_db():
    rv = sqlite3.connect(DATABASE)
    rv.row_factory = sqlite3.Row
    return rv

# database 取得
def get_db():
    if not hasattr(g,'sqlite_db'):
        g.sqlite_db = connect_db()
    return g.sqlite_db

@app.route('/')
def top():
    pog_lists = get_db().execute('select * from pog_lists where pog_id = 100').fetchall()
    return render_template ('test.html', pog_lists=pog_lists)

if __name__ == "__main__":
    app.run(debug=True)
    # app.run()

test.html

<!-- pog_lists から取得した結果を表示する -->
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>pog_lists</title>
  </head>
  <body>
    <h2>test pog_lists</h2>
    <table border="1">
      <tr>
        <th>pog_id</th>
        <th>dam_year</th>
        <th>horse_name</th>
        <th>gender</th>
        <th>coat_color</th>
        <th>url</th>
        <th>sire</th>
        <th>dam</th>
        <th>maternal_grandfather</th>
        <th>breeder</th>
      </tr>

      <!-- pog_lists は test.py で定義している pog_lists -->
      {% for pog in pog_lists %}
      <tr>
        <td>{{pog.pog_id}}</td>
        <td>{{pog.dam_year}}</td>
        <td>{{pog.horse_name}}</td>
        <td>{{pog.gender}}</td>
        <td>{{pog.coat_color}}</td>
        <td>{{pog.url}}</td>
        <td>{{pog.sire}}</td>
        <td>{{pog.dam}}</td>
        <td>{{pog.maternal_grandfather}}</td>
        <td>{{pog.breeder}}</td>
      </tr>
    {% endfor %}
    </table>
  </body>
</html>

エラー原因

本来参照してほしい DB が参照されていないことが原因だった。
画面表示した際、作成したものとは別の pog.db が作成されていた。sqlite3.connect() で対象の DB が存在しない場合、新たに DB を作成する。そのため、新たに作成された 0 KB の pog.db を参照して pog_lists テーブルは存在しないと言っていたということだった。

対処

  • test.py の DATABASE の設定値を修正した
  • test.py と pog.db は同じ階層にあるため、'pog.db' と設定すればよいと思っていたが、動き的にはそうではなかった

修正後の test.py

from  flask import Flask, render_template, g
import sqlite3
DATABASE='pog-draft-project/pog.db'

app = Flask(__name__)

# database 接続
def connect_db():
    rv = sqlite3.connect(DATABASE)
    rv.row_factory = sqlite3.Row
    return rv

# database 取得
def get_db():
    if not hasattr(g,'sqlite_db'):
        g.sqlite_db = connect_db()
    return g.sqlite_db

@app.route('/')
def top():
    pog_lists = get_db().execute('select * from pog_lists where pog_id = 100').fetchall()
    return render_template ('test.html', pog_lists=pog_lists)

if __name__ == "__main__":
    app.run(debug=True)
    # app.run()

SQLite で躓いたこと(import csv)

普段 DB に触れることがないから全然進まない。試行錯誤したこと備忘録。
一人で作業をしていると、何をやっているのか/やってきたのかだんだんわからなくなってくる。

サマリ

  • とある CSV 形式のデータを作成したテーブルにインポートしたかったが、当初想定した import コマンドで取り込む方法では取り込めなかった。
  • ID の自動採番や DEFAULT 値の設定が効かなさそう。(試した限りは無理そうだった)
  • 代替策として、CSV インポートではなく INSERT 文を作成・実行した。
  • INSERT 文は、ID のカラムと DEFAULT 値を設定したいカラムは除外した。
  • 当初は CSV 取り込めばいいじゃん ♪ というノリだったが、はまってしまった。結果論、初歩的なことを学んだのでよかったが。

やりたかったこと

  • CSV 形式のデータを作成済みのテーブルにインポートする。
  • 作成済みのテーブルは、CSV データのカラムに加えて、先頭にID(int / PK)、末尾に最終更新日時の列を追加している。

確認・実施したこと

※結果論、あまり本質的な点ではなかった。

  • CSV データのヘッダを除く(作成済みのテーブルにインポートする場合、先頭行が見出しではなくデータとみなされるため)
  • CSV データの EOF を正しい位置にする(データの最終行の末尾に EOF があること)
  • CSV データのカラム数と作成したテーブルのカラム数をそろえる(CSV を加工)
  • CSV データの各値をダブルクォーテーションで囲む(シングルクォーテーションだとそれがついた値で格納されるため NG)

作成したテーブル(サンプル)

-- SQLite
CREATE TABLE test2(
user_id integer PRIMARY KEY , 
user_name text,
user_role text DEFAULT '0',
updated_at timestamp DEFAULT (datetime(CURRENT_TIMESTAMP, 'localtime'))
);

CSV データ(test.csv

"1","a","",""
"2","b","",""
,"","",""
"","","",""

① import コマンドで CSV をインポートした場合 → NG

import コマンド

> sqlite3 panco.db
sqlite> .mode csv
sqlite> .import test.csv test2

実行結果

1行目、2行目のみが INSERT された。しかし、DEFAULT 値が空文字。CSVの通りなので当然ではある。

3行目、4行目は user_id の型がテーブル定義と CSV とで異なるため INSERT 失敗。(たぶん)

SQL で INSERT した場合(user_name のみ設定)→ 期待通り

INSERT 文

-- SQLite
INSERT INTO test2 (user_name)
VALUES ('a'),
('b');

実行結果

全カラムに期待通りのレコードが入った。

  • user_id は INTEGER かつ PK のため自動採番された
  • user_name は SQL の通り
  • user_role, updated_at は DEFAULT 値が付与された

閑話 今週のお題について「最近みた夢」

今週のお題「夢」

暇つぶし兼お勉強で開発中の Webアプリにて自分を疑うような設計ミスに気づき、萎えている今日。

設計中に気づいたのは不幸中の幸いか。でもその気づきすら間違っているんじゃないか、、などと思ってしまう。

さらにここ数日は謎の微熱でなんだか冴えないので、今週のお題について書いてみる。

2日前、ちょうどその設計ミスを混入させた日の夜に見た夢。

実母が藁人形を打ち付けている。

藁人形というと樹海で木に打ち付けているシーンが一般的だが、木ではなく私自身が杭になっていた。つまり、母が私に藁人形を打ち付けていた。

母に対して何の悪いイメージもないのに、どうしてこんな夢を見たのだろう。その日は熟睡できるはずもなく、変な時間に起きてそのまま朝になってしまった。

今日はちゃんと寝れるといいな☆彡