文字列を暗号化する

本文介绍了一种使用共有密钥暗号方式对字符串进行加密和解密的方法。通过创建DESCryptoServiceProvider对象并利用MemoryStream来处理加密过程,实现了字符串到加密字符串的转换,并提供了相应的VB.NET代码示例。

 http://dobon.net/vb/dotnet/string/encryptstring.html

共有キー暗号方式により文字列(String型データ)を暗号化し、文字列として取得する方法を紹介します。

方法は「ファイルを暗号化する」と同じですが、「ファイルを暗号化する」ではファイルから読み込んだデータを暗号化し、ファイルに書き込んでいるため、CryptoStreamオブジェクトを作成する際に、暗号化の対象とするストリームとしてFileStreamオブジェクトを指定していましたが、ここではMemoryStreamオブジェクトを指定することにします。

 

[VB.NET]
''' <summary>
''' 文字列を暗号化する
''' </summary>
''' <param name="str">暗号化する文字列</param>
''' <param name="key">パスワード</param>
''' <returns>暗号化された文字列</returns>
Public Shared Function EncryptString(ByVal str As String, _
                                ByVal key As String) As String
    '文字列をバイト型配列にする
    Dim bytesIn As Byte() = System.Text.Encoding.UTF8.GetBytes(str)

    'DESCryptoServiceProviderオブジェクトの作成
    Dim des As New System.Security.Cryptography.DESCryptoServiceProvider

    '共有キーと初期化ベクタを決定
    'パスワードをバイト配列にする
    Dim bytesKey As Byte() = System.Text.Encoding.UTF8.GetBytes(key)
    '共有キーと初期化ベクタを設定
    des.Key = ResizeBytesArray(bytesKey, des.Key.Length)
    des.IV = ResizeBytesArray(bytesKey, des.IV.Length)

    '暗号化されたデータを書き出すためのMemoryStream
    Dim msOut As New System.IO.MemoryStream
    'DES暗号化オブジェクトの作成
    Dim desdecrypt As System.Security.Cryptography.ICryptoTransform = _
        des.CreateEncryptor()
    '書き込むためのCryptoStreamの作成
    Dim cryptStreem As New System.Security.Cryptography.CryptoStream( _
        msOut, desdecrypt, _
        System.Security.Cryptography.CryptoStreamMode.Write)
    '書き込む
    cryptStreem.Write(bytesIn, 0, bytesIn.Length)
    cryptStreem.FlushFinalBlock()
    '暗号化されたデータを取得
    Dim bytesOut As Byte() = msOut.ToArray()

    '閉じる
    cryptStreem.Close()
    msOut.Close()

    'Base64で文字列に変更して結果を返す
    Return System.Convert.ToBase64String(bytesOut)
End Function

''' <summary>
''' 暗号化された文字列を復号化する
''' </summary>
''' <param name="str">暗号化された文字列</param>
''' <param name="key">パスワード</param>
''' <returns>復号化された文字列</returns>
Public Shared Function DecryptString(ByVal str As String, _
                            ByVal key As String) As String
    'DESCryptoServiceProviderオブジェクトの作成
    Dim des As New System.Security.Cryptography.DESCryptoServiceProvider

    '共有キーと初期化ベクタを決定
    'パスワードをバイト配列にする
    Dim bytesKey As Byte() = System.Text.Encoding.UTF8.GetBytes(key)
    '共有キーと初期化ベクタを設定
    des.Key = ResizeBytesArray(bytesKey, des.Key.Length)
    des.IV = ResizeBytesArray(bytesKey, des.IV.Length)

    'Base64で文字列をバイト配列に戻す
    Dim bytesIn As Byte() = System.Convert.FromBase64String(str)
    '暗号化されたデータを読み込むためのMemoryStream
    Dim msIn As New System.IO.MemoryStream(bytesIn)
    'DES復号化オブジェクトの作成
    Dim desdecrypt As System.Security.Cryptography.ICryptoTransform = _
        des.CreateDecryptor()
    '読み込むためのCryptoStreamの作成
    Dim cryptStreem As New System.Security.Cryptography.CryptoStream( _
        msIn, desdecrypt, _
        System.Security.Cryptography.CryptoStreamMode.Read)

    '復号化されたデータを取得するためのStreamReader
    Dim srOut As New System.IO.StreamReader( _
        cryptStreem, System.Text.Encoding.UTF8)
    '復号化されたデータを取得する
    Dim result As String = srOut.ReadToEnd()

    '閉じる
    srOut.Close()
    cryptStreem.Close()
    msIn.Close()

    Return result
End Function

''' <summary>
''' 共有キー用に、バイト配列のサイズを変更する
''' </summary>
''' <param name="bytes">サイズを変更するバイト配列</param>
''' <param name="newSize">バイト配列の新しい大きさ</param>
''' <returns>サイズが変更されたバイト配列</returns>
Private Shared Function ResizeBytesArray(ByVal bytes() As Byte, _
                            ByVal newSize As Integer) As Byte()
    Dim newBytes(newSize - 1) As Byte
    If bytes.Length <= newSize Then
        Dim i As Integer
        For i = 0 To bytes.Length - 1
            newBytes(i) = bytes(i)
        Next i
    Else
        Dim pos As Integer = 0
        Dim i As Integer
        For i = 0 To bytes.Length - 1
            newBytes(pos) = newBytes(pos) Xor bytes(i)
            pos += 1
            If pos >= newBytes.Length Then
                pos = 0
            End If
        Next i
    End If
    Return newBytes
End Function
【概要】 Bitbucket登録したPKGついてJenkinsてビルドし、登録元のソフトとリリースするファイルが一致すること確認する。 ① JenkinsビルドJOB作成 ② CHKファイル生成 ③ ビルド後PKG比較 ④ TAG設定 【前提】 ■ベースビルド検証 制御ソフトコードおよび意匠ソフトコードのブランチが作成済みであること ■各イベントでのPKG作成 制御、意匠ともmasterブランチ対応要件のコードが反映されていること 【手順】 ① JenkinsビルドJOB作成 対象車両のビルドJOB作成するため、新規ジョブ作成選択する フォルダ選択し、JOB名入力する 作成したJOBフォルダの中で、さらJOB作成する 名前入力し、フリースタイル・プロジェクトのビルド選択する 作成されたこと確認 作成したJenkinsJOB選択、各種項目設定したのちパラメータ付きビルド実行する 設定内容ついては、JenkinsJOB_作成参考資料シート参照 ② CHKファイル生成 ビルド完了後ワークスペース選択し、PKGダウンロードする PKGダウンロード後は以下のシートの530-667行目参照 ビルド手順補足 ③ ビルド後PKG比較 →初回PKG登録時のみ実施する ダウンロード後、SVNからベースした登録PKG取得し比較する レポート生成選択後、ファイル生成場所と形式(CSV)選択する 以下のような比較結果作成し、PKG構成ファイルおよび生成物問題ないこと確認する Box\bxd-meter_develop_honda\01_開発\TZAA\ISB(33FA)\XX_Jenkins立ち上げ\02_PKG作成\ア大 33FA_A332_before_vs_after.xlsx 差分箇所抽出シート:PKG全体比較し、差分および片方のみ存在するファイルついて確認する dst_配下比較結果シート:リリースしたソフト(CHKファイルや暗号ファイル)比較し一致すること確認する ④ TAG設定 ビルド完了後、制御ソフトおよび意匠ソフトのmasterブランチ対してTAG設定しプッシュする タグ設定したいコミットの上で右クリックし、タグ作成選択する 黄色の枠でTAGが生成されていること確認する 設定したTAGBitbucketへ反映させる ▼選択し、タグプッシュ選択する Bitbucket設定したタグが反映されていること確認する END
11-18
package jp.co.sej.ssc.cl.sclb1040; import com.google.crypto.tink.Aead; import com.google.crypto.tink.KmsClient; import com.google.crypto.tink.aead.AeadConfig; import com.google.crypto.tink.aead.AeadParameters; import com.google.crypto.tink.aead.KmsEnvelopeAead; import com.google.crypto.tink.aead.PredefinedAeadParameters; import com.google.crypto.tink.integration.gcpkms.GcpKmsClient; import jakarta.annotation.PostConstruct; import lombok.Getter; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.env.Environment; /* * 暗号部品用のTink設定クラス * * Bean登録(コンストラクタ)のタイミングで、時間のかかる初期処理行っておく。 */ @Log4j2 public final class Sclb1041 { //// キーテンプレート名 //private final String KEYTEMPLATE_NAME = "AES256_GCM"; //// キーテンプレート //@Getter //private KeyTemplate tinkKeyTemplate; private static final String ENVELOPE_ENCRYPTION_APP_YAML_SECTION_FIRST_KEK_URI = "envelope-encryption.keys.kek-uri.kek-uri-1"; @Getter //2023/12/05追加 定義済みAeadパラメータ private AeadParameters predefinedAeadParameters = PredefinedAeadParameters.AES256_GCM; /* * KMS未使用モード * true:KMS未使用モード / false:KMS使用モード(デフォルト)) */ @Value("${envelope_encryption_kms_use_disabled:false}") @Getter private boolean kmsUseDisabled; /* * サービスアカウント(SA)キーファイルパス */ @Value("${envelope_encryption_gcp_credential_filepath:}") @Getter private String gcpCredentialFilePath; @Autowired private Environment environment; /* * コンストラクタ * * @throws RuntimeException */ public Sclb1041() throws RuntimeException { try { // Tink初期(サポートするAEADキータイプ登録) AeadConfig.register(); //// キーテンプレート //tinkKeyTemplate = KeyTemplates.get(KEYTEMPLATE_NAME); } catch (Exception e) { throw new RuntimeException( Sclb1050.SCLM4001 + " " + Sclb1050.SCLM40011 + ":initialization failure in bean registration.", e); } } /* * KEK(kek-uri-1)のGCPリソースID取得 * * @param kekUriName application.ymlで指定するKEK(kek-uri-1)の名前 * @return application.ymlで指定するKEK(kek-uri-1)のGCPリソースID * @throws IllegalStateException */ private String getPrepareKekUriPath() { return environment.getProperty(ENVELOPE_ENCRYPTION_APP_YAML_SECTION_FIRST_KEK_URI); } /* * Spring Bootアプリケーション起動後の処理 * Spring Bootアプリケーション起動時Cloud KMSへのアクセス行うことで、業務処理よる初回Cloud KMSアクセス高速する。 * ・暗号処理使用するKEKは、"kek-uri-1"で固定。 * ・暗号処理使用するデータはダミーデータとして0x00のみのバイト配使用。 */ @PostConstruct protected void prepare() { if (!kmsUseDisabled) { // AEADプリミティブ Aead aead = null; Aead remoteAead = null; log.debug("preparing encryption started."); try { // キーセット //keysetHandle = KeysetHandle // .generateNew(KmsEnvelopeAeadKeyManager.createKeyTemplate(getPrepareKekUriPath(), // tinkKeyTemplate)); // KMSクライアント KmsClient kmsClient = new GcpKmsClient(); if (gcpCredentialFilePath.isEmpty()) { // サービスアカウントキー未使用(GCP環境で実行)(defaultCredentials()) //GcpKmsClient.register(Optional.of(getPrepareKekUriPath()), Optional.empty()); kmsClient.withDefaultCredentials(); } else { // サービスアカウントキー使用(withCredentials(gcpCredentialFilename)) //GcpKmsClient.register(Optional.of(getPrepareKekUriPath()), Optional.of // (gcpCredentialFilePath)); kmsClient.withCredentials(gcpCredentialFilePath); } // AEADプリミティブ remoteAead = kmsClient.getAead(getPrepareKekUriPath()); //aead = keysetHandle.getPrimitive(Aead.class); aead = KmsEnvelopeAead.create(getPredefinedAeadParameters(), remoteAead); } catch (Exception e) { log.debug("Tink initialization failure in preparing encryption.KEK name:kek-uri-1." + e.getMessage()); } // 暗号(暗号データ後続利用無し。ダミーデータとして0x00のみのバイト配使用。) final byte[] preInitByte = {0}; try { if (aead != null) { aead.encrypt(preInitByte, null); } } catch (Exception e) { log.debug("preparing encryption failed.KEK name:kek-uri-1." + e.getMessage()); } log.debug("preparing encryption end."); } } } 42:package jp.co.sej.ssc.cl.sclb1040; import com.google.crypto.tink.Aead; import com.google.crypto.tink.KmsClient; import com.google.crypto.tink.aead.KmsEnvelopeAead; import com.google.crypto.tink.integration.gcpkms.GcpKmsClient; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; import org.springframework.core.env.Environment; import org.springframework.stereotype.Component; import org.springframework.util.SerializationUtils; /** * 暗号部品のロジッククラス */ @Configuration @Component @Scope("prototype") @Log4j2 public class Sclb1042 { // 定数 private static final String ENVELOPE_ENCRYPTION_APP_YAML_SECTION_KEK_URI = "envelope-encryption.keys.kek-uri."; // AEADプリミティブ //private KeysetHandle keysetHandle = null; private Aead aead = null; private Aead remoteAead = null; // Tink設定 @Autowired private Sclb1041 tinkConfig; @Autowired private Environment environment; /* * KEKのGCPリソースID取得 * * @param kekUriName application.ymlで指定するKEKの名前 * @return application.ymlで指定するKEKのGCPリソースID * @throws IllegalStateException */ private String getkekUriPath(String kekUriName) throws IllegalStateException { return environment.getRequiredProperty( ENVELOPE_ENCRYPTION_APP_YAML_SECTION_KEK_URI + kekUriName); } /* * Tink初期 * * KEK未使用モードの場合も、application.yml上のKEKと暗号対象項目定義のチェックは行う。 * * @param kekUriName application.ymlで指定するKEKの名前 * @throws RuntimeException */ private void init(String kekUriName) throws RuntimeException { String kekUriPath = ""; try { kekUriPath = getkekUriPath(kekUriName); } catch (Exception e) { throw new RuntimeException(Sclb1050.SCLM4001 + " " + Sclb1050.SCLM40012 + ":initialization failure. KEK URI does not defined on application.yml. KEK name:" + kekUriName + ".", e); } // GCP接続処理と暗号鍵関連の初期 if (!tinkConfig.isKmsUseDisabled()) { try { // キーセット //keysetHandle = KeysetHandle.generateNew( // KmsEnvelopeAeadKeyManager.createKeyTemplate(kekUriPath, tinkConfig // .getTinkKeyTemplate())); // KMSクライアント KmsClient kmsClient = new GcpKmsClient(); if (tinkConfig.getGcpCredentialFilePath().isEmpty()) { // サービスアカウントキー未使用(GCP環境で実行)(defaultCredentials()) //GcpKmsClient.register(Optional.of(kekUriPath), Optional.empty()); kmsClient.withDefaultCredentials(); } else { // サービスアカウントキー使用(withCredentials(gcpCredentialFilename)) //GcpKmsClient.register(Optional.of(kekUriPath), Optional.of(tinkConfig // .getGcpCredentialFilePath())); kmsClient.withCredentials(tinkConfig.getGcpCredentialFilePath()); } // AEADプリミティブ remoteAead = kmsClient.getAead(kekUriPath); //aead = keysetHandle.getPrimitive(Aead.class); aead = KmsEnvelopeAead.create(tinkConfig.getPredefinedAeadParameters(), remoteAead); } catch (Exception e) { throw new RuntimeException( Sclb1050.SCLM4001 + " " + Sclb1050.SCLM40013 + ":Tink initialization failure. KEK name:" + kekUriName, e); } } } /* * 暗号(エンベロープ暗号) * * 暗号対象データバイト配シリアライズし、Tinkでエンベロープ暗号する。 * * @param encryptTargetData 暗号対象データ * @param kekUriName application.ymlで指定するKEKの名前 * @return 暗号済みデータ * @throws RuntimeException */ public byte[] envelopeEncrypt(String encryptTargetData, String kekUriName) throws RuntimeException { return envelopeEncrypt((Object) encryptTargetData, kekUriName); } /* * 暗号(エンベロープ暗号) * * 暗号対象データバイト配シリアライズし、Tinkでエンベロープ暗号する。 * * @param encryptTargetData 暗号対象データ * @param kekUriName application.ymlで指定するKEKの名前 * @return 暗号済みデータ * @throws RuntimeException */ public byte[] envelopeEncrypt(Character encryptTargetData, String kekUriName) throws RuntimeException { return envelopeEncrypt((Object) encryptTargetData, kekUriName); } /* * 暗号(エンベロープ暗号) * * 暗号対象データバイト配シリアライズし、Tinkでエンベロープ暗号する。 * * @param encryptTargetData 暗号対象データ * @param kekUriName application.ymlで指定するKEKの名前 * @return 暗号済みデータ * @throws RuntimeException */ public byte[] envelopeEncrypt(Integer encryptTargetData, String kekUriName) throws RuntimeException { return envelopeEncrypt((Object) encryptTargetData, kekUriName); } /* * 暗号(エンベロープ暗号) * * 暗号対象データバイト配シリアライズし、Tinkでエンベロープ暗号する。 * * @param encryptTargetData 暗号対象データ * @param kekUriName application.ymlで指定するKEKの名前 * @return 暗号済みデータ * @throws RuntimeException */ public byte[] envelopeEncrypt(int encryptTargetData, String kekUriName) throws RuntimeException { return envelopeEncrypt((Object) encryptTargetData, kekUriName); } /* * 暗号(エンベロープ暗号) * * 暗号対象データバイト配シリアライズし、Tinkでエンベロープ暗号する。 * * @param encryptTargetData 暗号対象データ * @param kekUriName application.ymlで指定するKEKの名前 * @return 暗号済みデータ * @throws RuntimeException */ public byte[] envelopeEncrypt(java.sql.Timestamp encryptTargetData, String kekUriName) throws RuntimeException { return envelopeEncrypt((Object) encryptTargetData, kekUriName); } /* * 暗号(エンベロープ暗号) * * 暗号対象データバイト配シリアライズし、Tinkでエンベロープ暗号する。 * * @param encryptTargetData 暗号対象データ * @param kekUriName application.ymlで指定するKEKの名前 * @return 暗号済みデータ * @throws RuntimeException */ public byte[] envelopeEncrypt(java.sql.Date encryptTargetData, String kekUriName) throws RuntimeException { return envelopeEncrypt((Object) encryptTargetData, kekUriName); } /* * 暗号(エンベロープ暗号) * * 暗号対象データバイト配シリアライズし、Tinkでエンベロープ暗号する。 * * @param encryptTargetData 暗号対象データ * @param kekUriName application.ymlで指定するKEKの名前 * @return 暗号済みデータ * @throws RuntimeException */ public byte[] envelopeEncrypt(Object encryptTargetData, String kekUriName) throws RuntimeException { try { // Tink初期 init(kekUriName); if (!tinkConfig.isKmsUseDisabled()) { // Tinkよる暗号 byte[] afterEncryptBytes = aead.encrypt(serializeObject(encryptTargetData), null); return afterEncryptBytes; } else { // バイト配 byte[] seraializedBytes = serializeObject(encryptTargetData); return seraializedBytes; } } catch (Exception e) { // 暗号処理でエラー発生 throw new RuntimeException( Sclb1050.SCLM4002 + " " + Sclb1050.SCLM40021 + ":encryption failure.", e); } } /* * 復号(エンベロープ暗号) * * Tinkでエンベロープ暗号された復号対象データ(バイト配)復号し、Objectインスタンスデシリアライズする。 * * @param decryptTargetData 復号対象データ * @param clazz 復号済みデータの想定型 * @param kekUriName application.ymlで指定するKEKの名前 * @return 復号済みデータ * @throws RuntimeException */ public <T> Object envelopeDecrypt(byte[] decryptTargetData, Class<T> clazz, String kekUriName) throws RuntimeException { // 復号 Object decTargetData = envelopeDecrypt(decryptTargetData, kekUriName); if (decTargetData.getClass() != clazz) { // 復号後オブジェクトの型が使用メソッドの想定型と不一致 throw new RuntimeException(Sclb1050.SCLM4003 + " " + Sclb1050.SCLM40034 + ":Object type does not match the expected object type of the using method.Expected: " + clazz.getName() + "/Decrypted: " + decTargetData.getClass().getName() + "."); } return decTargetData; } /* * 復号(エンベロープ暗号) * * Tinkでエンベロープ暗号された復号対象データ(バイト配)復号し、Objectインスタンスデシリアライズする。 * * @param decryptTargetData 復号対象データ * @param kekUriName application.ymlで指定するKEKの名前 * @return 復号済みデータ * @throws RuntimeException */ public Object envelopeDecrypt(byte[] decryptTargetData, String kekUriName) throws RuntimeException { Object decTargetData = null; try { // Tink初期 init(kekUriName); if (!tinkConfig.isKmsUseDisabled()) { // Tinkよる復号 decTargetData = deserializeObject(aead.decrypt(decryptTargetData, null)); } else { // バイト配からのデシリアライズ decTargetData = deserializeObject(decryptTargetData); } } catch (Exception e) { // 復号処理でエラー発生 throw new RuntimeException( Sclb1050.SCLM4003 + " " + Sclb1050.SCLM40031 + ":decryption failure.", e); } return decTargetData; } /* * Tinkてエンベロープ暗号されたデータ文字(String)として復号する * * @param decryptTargetData 復号対象データ * @param kekUriName application.ymlで指定するKEKの名前 * @return 復号データ */ public String envelopeDecryptString(byte[] decryptTargetData, String kekUriName) { Object obj = envelopeDecrypt(decryptTargetData, String.class, kekUriName); return (String) obj; } /* * Tinkてエンベロープ暗号されたデータ文字(char)として復号する * * @param decryptTargetData 復号対象データ * @param kekUriName application.ymlで指定するKEKの名前 * @return 復号データ */ public char envelopeDecryptChar(byte[] decryptTargetData, String kekUriName) { Object obj = envelopeDecrypt(decryptTargetData, char.class, kekUriName); return (char) obj; } /* * Tinkてエンベロープ暗号されたデータ数値(Integer)として復号する * * @param decryptTargetData 復号対象データ * @param kekUriName application.ymlで指定するKEKの名前 * @return 復号データ */ public Integer envelopeDecryptInteger(byte[] decryptTargetData, String kekUriName) { Object obj = envelopeDecrypt(decryptTargetData, Integer.class, kekUriName); return (Integer) obj; } /* * Tinkてエンベロープ暗号されたデータ数値(int)として復号する * * @param decryptTargetData 復号対象データ * @param kekUriName application.ymlで指定するKEKの名前 * @return 復号データ */ public int envelopeDecryptInt(byte[] decryptTargetData, String kekUriName) { Object obj = envelopeDecrypt(decryptTargetData, int.class, kekUriName); return ((Integer) obj).intValue(); } /* * Tinkてエンベロープ暗号されたデータ日時(java.sql.Timestamp)として復号する * * @param decryptTargetData 復号対象データ * @param kekUriName application.ymlで指定するKEKの名前 * @return 復号データ */ public java.sql.Timestamp envelopeDecryptTimestamp(byte[] decryptTargetData, String kekUriName) { Object obj = envelopeDecrypt(decryptTargetData, java.sql.Timestamp.class, kekUriName); return (java.sql.Timestamp) obj; } /* * Tinkてエンベロープ暗号されたデータ日付(java.sql.Date)として復号する * * @param decryptTargetData 復号対象データ * @param kekUriName application.ymlで指定するKEKの名前 * @return 復号データ */ public java.sql.Date envelopeDecryptDate(byte[] decryptTargetData, String kekUriName) { Object obj = envelopeDecrypt(decryptTargetData, java.sql.Date.class, kekUriName); return (java.sql.Date) obj; } /* * 暗号対象データのシリアライズ * * @param obj シリアライズ対象データ(Serializable) * @return シリアライズ済みデータ * @throws RuntimeException */ private byte[] serializeObject(Object obj) { if (obj instanceof Serializable) { return SerializationUtils.serialize(obj); } else { throw new RuntimeException( Sclb1050.SCLM4002 + " " + Sclb1050.SCLM40022 + ":object must Serializable."); } } /* * 復号済みデータのデシリアライズ * * @param data デシリアライズ対象データ * @throws IOException * @throws ClassNotFoundException */ private Object deserializeObject(byte[] data) throws IOException, ClassNotFoundException { try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data))) { return ois.readObject(); } } }
10-14
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值